diff --git a/active.md b/active.md index 15f1ab91..43cbdecb 100644 --- a/active.md +++ b/active.md @@ -12,6 +12,7 @@ ## 当前状态快照(收口批先更新本节,再追加台账;≤10 行) - **更新于**:2026-10-07(**迭代18增卡⑦新冒险种子随机化落籍 master 7a7d873**,worktree g-plan2-seedcard 单提交 997bcc9+主区 merge --no-ff,工作日志 382;纯文档零代码零测试):用户令——现状实查 main.gd:1210 种子框空硬编码 42=每次新冒险同种子全程复现;卡⑦=默认分支改随机生成+调试浮窗种子框保留手动覆盖,断点续玩/冒烟锚/⑥收官批三面零影响考证在册;裁点两项随任务书(首次冒险教学态是否随机/种子进 UI 展示位);⑥门改「内容卡①-⑤⑦出口」;下一步=迭代 18 任务书讨论 +- **更新于**:2026-10-06(**内容数据驱动扩展批实施完成落籍 master**(worktree iter17-dd 分支 iter17-dd-20261006,主区 merge --no-ff),工作日志 383〔预配号 381 撞号改登——381/382 系并行迭代 18 规划批与增卡⑦两批先落,先例同台账 374〕):迭代 17 卡⓪ 当日立项当日落地——T1 效果引擎框架+16 发射点(7d157b3)/T2 词汇落码(225765f)系前任完成,续跑会话接手 T3 断点三脏文件收口:T3 专长 27 行 triggers 全量迁移+旧 23 挂点枚举消亡+消费点拆除+文案入表+断言面随迁(e28a6cb,承接修正四处=perfect_defense 事实出域/has 算子语义/漏写 _harmful_adjacent/counter set 键 bool 吞零);T4+T5 秘宝六件按 id 硬编码清零(淬毒 clamp_bonus 模式路由/偏运经 reward_roll/晨令 round_start 订阅+when 行字段定影/惊巢 halve_pct/殉爆订阅开单/掘进蓄能受理闸 counter_spend 订阅)+令牌兑付 match 收敛进效果类型路由表(a33ddaf);闸=冒烟 800 项 0 失败+单元 17 套+探针闸含 click 全绿+MC 二档对拍 40 局同种子**逐位一致**(pre/post 锚 83000 段留盘 sim-data/);T7 文档换代=奇物 v2.8/秘宝 v1.0/敌人系统 v0.14+registry 两件+06 收口+roadmap 卡⓪行+出口报告(design/qa/iter17-datadrive-exit-report-2026-10-06.md);遗留人闸两项随报告(掘进蓄能模判定留编排器侧=词汇表边界/主动件 kind 词族全量 rename 不随批)+实机目检归用户。**下一步待用户=实机抽查**。 - **更新于**:2026-10-06(**迭代18规划批落籍 master 018435e**,worktree g-plan2-iter18plan 单提交 0d80626+主区 merge --no-ff,工作日志 381;纯文档零代码零测试):用户令——M4 数值批移 Alpha 阶段最后=迭代 18 ⑥收官卡(门=内容卡①-⑤出口,内容补全先行、统一数值调整殿后)+五内容卡立项:①奇物内容补全·文档先行(charge 补件设计〔实查现恰 3 件〕+06 资源入池条收窄收口〔刷新件与门票实已随迭代 15②入池〕)/②呈现优化六件(与迭代 16 语义零重合=其 token 下游)/③武器锻造系统设计提案(06 新立专条)/④生涯实装(门=卡④提纲评审)/⑤混编遭遇首批投放(MC 取证顺延⑥收官批);06 七处+07 一处状态随迁;资产升档批优先级再降维持挂起;**下一步待用户=讨论迭代 18 各卡任务书**(B 文档批零代码成立、C 与迭代 16 零重合两问结论已呈报) - **更新于**:2026-10-06(**暂停层只读入口批完成**,迭代 17 卡③功能性 UI 交互批——纯呈现零数值改动=零 MC,worktree iter17-pause-20261006 基点 44328b1,两会话接力〔前任完成 T0/T1+组 B 代码后额度中断,续跑会话收尾跑闸+T3-T5〕,全部提交在 worktree 内未合并,台账 380):战斗屏/关卡地图屏各一枚「暂停」常驻角标开只读层(余额徽+三柜台进度+「继续」,零购买零写通路;badge 已工厂化入 ui_kit,营地钥石徽同步迁移观感零变化);他模态开着角标置灰不可点(has_open_excluding)。闸=冒烟 788/0(基线 783/0)+run_all 17 套+四探针全绿(含输入锁 case⑯/只读巡检/零动效契约/对比度新对)+doc 三闸零新增;存量移交两项=熵增闸 design/diagrams 越界(00712d5 落籍漏步,主区同红)+06/07 gdd_lint 存量注记红(本批零触碰)。出口报告=design/qa/iter17-pause-exit-report-2026-10-06.md。**下一步待用户=实机目检**(design/qa/iter17-pause-human-gate-checklist-2026-10-06.md,重点含排播排空副作用确认项 D2);过闸后合并落 master、06 目标条销号。 - **更新于**:2026-10-06(**生涯界面设计提纲批完成**,迭代 17 卡④纯设计批——零代码、零数据面、零测试执行,worktree iter17-career-20261006 基点 fc4dbde,全部提交在 worktree 内未合并):设计提纲已按 P0 甲裁定写成正式设计文档 design/gdd/systems/跨局循环/生涯.md v0.1(状态=范围草案),五节写全(信息架构/承载内容清单/与既有界面关系/分期实装建议/待裁点);P0-P5 六个已裁决结论全部落进对应章节,任务书 §2 十条现状事实(F1-F10)逐条对照代码复核全绿;复核新发现一项待用户裁决点「P6 营地底栏『最近冒险』条带去留」(数据源与生涯历史回看同一份存档摘要、条带拼行无上限,扩容后会爆——建议期二时撤下、逐局回看整体归生涯)。同批改动:02-系统清单.md 根索引补生涯行与 CAREER 代号行、06 生涯条状态行改「设计提纲已成文待评审」、roadmap 迭代 17 卡④行改已交付待评审、出口报告落 design/qa/iter17-career-exit-report-2026-10-06.md。**下一步待用户=评审设计提纲**(评审时随裁 P6);评审通过后期一/二/三实装批另行立项,本批未预占任何排期。 @@ -874,3 +875,4 @@ 380. **暂停层只读入口批完成(2026-10-06,迭代 17 卡③功能性 UI 交互批——纯呈现零数值改动=零 MC;worktree g-plan2-iter17-pause 分支 iter17-pause-20261006,基点 44328b1,两会话接力:前任会话完成 T0/T1+组 B 代码后因额度中断,续跑会话收尾组 B 跑闸+T3-T5;本条为编排会话预配号,落籍序以合并为准)**:局内暂停层交付——战斗屏 HUD 右下角与关卡地图屏顶栏各一枚「暂停」常驻角标(P1 丁,debug 角标同形制紧凑钮;主菜单不误现),点击开只读层(P2 甲=登记 modal_stack 登记册设置层同档带 dim 遮罩、确认弹窗之前),层内=钥石余额徽+属性锻造三行已购等级+道具工坊六行解锁态+天赋树已购概览+「继续」关闭钮(P3 甲,行序复用营地屏同序),零购买零切换零写通路。badge 按 Q10 甲工厂化:UiKit 新增 KeystoneBadge 工厂件,营地顶栏钥石徽本地拼装同步迁工厂调用(观感零变化,消单点重复);零新素材零新色值零新字体。守卫=其他模态开着两角标置灰不可点(modal_stack 新增 has_open_excluding 排除式开态查询——地图屏自身开态不计;组合根 _on_pause_badge_pressed 处理器兜底+_sync_pause_entry 单一同步点挂模态开合单口与屏级互斥口五路径)。断言四面=ui_struct(pause 结构+只读性契约〔游戏语义信号十二名清单巡检,白名单外零连接,合法连接恰 2〕+零动效契约〔子树零 AnimationPlayer+源码零 tween〕)+ui_input case⑯(角标真点击开层/锁期挖掘零穿透/关后恢复无残留——R3)+ui_click(战斗屏开合守卫+地图屏一次性 case)+ui_contrast(钥石徽深墨/烛光黄对)+冒烟投影一致性 5 断言(展示值==MetaProgression 访问器逐项——R4)。续跑首轮两处 L1 探针红均系断言口径(全集巡检误判引擎布局生命线连接/域恰 2 枚时零无旗格可取),修正一轮全绿无复发。闸=冒烟 788/0(基线 783/0,+5 本批)+run_all 17 套+probe_gate --click 全绿+ui_layout exit0+doc 三闸零新增;存量移交两项(非本批产生,主区同红)=熵增闸 design/diagrams 越界〔00712d5 总图批落籍未同批修白名单〕+06/07 gdd_lint 存量注记红〔本分支零触碰〕。代拍板五项=关闭钮「继续」/层内行序营地同序/角标文案与落点/断言 case 命名/巡检信号清单(目检清单 F 节追认单)。出口报告=design/qa/iter17-pause-exit-report-2026-10-06.md;**人闸=用户实机目检** design/qa/iter17-pause-human-gate-checklist-2026-10-06.md(A-H 八节,重点=双屏入口/守卫态/只读性/零动效/排播队列排空副作用确认项 D2——开层瞬间待播明细行瞬时入带与「暂停」直觉之差系既有弹层同款行为,待用户确认可接受)。 381. **迭代18规划批完成(2026-10-06,用户令——纯文档零代码零测试;worktree g-plan2-iter18plan 分支 iter18-plan-20261006 单提交 0d80626,主区 merge --no-ff 018435e,worktree 与分支已清)**:roadmap 迭代 18 重组——⑥M4 数值批移 Alpha 阶段最后=收官卡(门改「迭代 18 内容卡①-⑤出口——重锚窗口须收尽全部内容批行为变更」;标题增二次令注记=内容补全先行、统一数值调整殿后,内容卡若溢出后续迭代则本批随之顺延);新立五卡:①奇物内容补全·文档先行(charge 补件设计——实查 feats.tres 现恰 3 件〔整备军备/铁壁整备/厚积薄发,表注释自证〕;06「资源两个新种类入池」条收口核对——实查刷新次数〔treasure_items.tres 三选一刷新件行〕与专长获取门票〔shop.tres+adventure_table.gd 门票行〕均已随迭代 15②/三选一刷新批入池、该条现状文本滞后即日收窄;本卡纯文档零代码,落地批=纯 .tres 数据行估 0.5d 内,门=文档出口+用户裁件+迭代 17 卡⓪落籍避表结构撞车)/②呈现优化六件(与迭代 16 语义零重合=16 系样式对齐、本批系内容与观感改版,动的是 16 落好的 token 单点;(a) 流派名三处+档位呈现三路待裁/(b) L1 直画观感/(c) 高墙阴线/(d) 残余对比度三处/(e) L2L3 棋盘观感开工先定性素材轴 vs UI 轴/(f) 关卡节点图展示改版先出「哥布林弹珠」参考调研+方案人闸;随批义务=07⑮ 渲染像素五闸补跑)/③武器锻造系统设计提案(playnote 2026-10-06 用户提案=攻击力提升改造武器锻造〔剑/长枪/法杖×穿刺/魔法+轻甲→重甲/魔法甲〕,纯文档,06 新立专条,实装批另行立项,数值框架 provisional 终值归⑥)/④生涯界面实装(按卡④提纲 design/gdd/systems/跨局循环/生涯.md v0.1 实装,门=用户评审提纲+设计定稿)/⑤混编遭遇首批投放(07「后续内容批义务」落地,门=迭代 17 卡①落籍;读数义务随批登记 07、MC 取证跑批顺延⑥收官批统一执行);06 状态行七处随迁+07 混编义务编入注;同批用户裁决落文=B 奇物内容补充先文档(零代码结论成立)、C 呈现优化立项(与迭代 16 零重合)、D 资产升档优先级再降维持挂起(06 增降级注+playnote 立绘批评记为重启输入)、F 道具重设计同 B 先不处理、G 成长曲线/H L1 第二节点暂不讨论留 playnote;遗留提示=A 菜单开局传参缺陷〔main.gd:475 只传四键,本会话代码复核属实〕未入迭代 18(用户未议)——将来修复须排在⑥M4 之前,否则案C 后重锚再作废一次;**下一步待用户=讨论迭代 18 各卡任务书**。 382. **迭代18增卡⑦新冒险种子随机化(2026-10-07,用户令——纯文档零代码零测试;worktree g-plan2-seedcard 分支 iter18-seedcard-20261007 单提交 997bcc9,主区 merge --no-ff 7a7d873,worktree 与分支已清)**:用户指出现状=每次展开冒险种子固定不随机(实查 main.gd:1210 _seed_value() 调试种子框为空时硬编码返回 42——每次新冒险同一种子全程可复现;同日答用户问=核心循环全固定的 roguelike 不存在,固定种子只在每日挑战/调试特设模式)→ 卡⑦=新冒险主种子改随机生成(默认分支),调试浮窗种子输入框保留手动覆盖(_seed_value 唯一消费点不变,迭代 15 卡①既有读取面);三面零影响考证在册=断点续玩存档已载 master_seed 回填〔main.gd:447 现役〕/冒烟·MC·探针全链显式传种子〔锚零作废〕/单种子内行为不变、与⑥收官批零耦合排序自由;裁点两项随任务书=①首次冒险是否也随机〔现行 42+固定模板 0=首次冒险全程复现的教学态;A6 首遇教学位系节点模板面不受种子影响,但板面雷位/资源摇号会随种子变〕②种子是否进 UI 展示〔局摘要已携带 master_seed,展示=分享/日后每日挑战位候选〕;⑥M4 门同步改「迭代 18 内容卡①-⑤⑦出口」;**下一步待用户=迭代 18 任务书讨论**。 +383. **内容数据驱动扩展批完成(2026-10-06/07,迭代 17 卡⓪——效果引擎框架落地;worktree g-plan2-iter17-dd 分支 iter17-dd-20261006 基点 fc4dbde,五提交 7d157b3/225765f/e28a6cb/a33ddaf/6e7b5d1,两会话接力〔前任完成 T1 效果引擎框架+16 发射点与 T2 词汇落码后额度中断,续跑会话接手 T3 断点三脏文件收口并完成 T4-T7〕;本条为编排会话代记——预配号 381 撞号改登 383,381/382 系并行迭代 18 规划批与增卡⑦两批先占,先例同台账 374)**:内容效果行为从结算代码硬编码搬进数据行「订阅事件 on+条件 when+效果 do」三字段,EffectEngine 发布订阅解释器统一执行——16 发射点埋点+19 行 23 效果类型+组合规则 6 条+改写窗口单点;专长 27 行全量迁 triggers 订阅形态、旧 23 挂点枚举消亡、伤害控制器 13 处消费点拆除、秘宝六件按 id 硬编码清零(淬毒/偏运经 reward_roll/晨令/惊巢/殉爆/掘进蓄能受理闸)、令牌兑付 match 收敛进效果类型路由表、树规则键同面、文案表值化、断言面随迁净增 17;续跑承接修正四处(perfect_defense 事实出域/has 算子语义/漏写 _harmful_adjacent/counter set 键 bool 吞零)。闸=冒烟 800/0(基线 783/0,+17 本批)+run_all 17 套+probe_gate 含 click 全绿+MC 二档对拍 40 局同种子逐位一致(pre/post 锚 83000 段留盘 sim-data/,判据两套全过:确定性复放与冒烟=逐位、MC 统计量=同带)+doc 三闸/熵增存量红零新增。文档换代=奇物 v2.8/秘宝 v1.0/敌人系统 v0.14+registry 两件+06 收口+roadmap 卡⓪行换代;出口报告=design/qa/iter17-datadrive-exit-report-2026-10-06.md(代拍板八项随报告)。遗留=掘进蓄能积点模判定留编排器侧(发射点施加架构下无法行内表达,行为逐位一致,彻底行内化归词汇表扩展批裁决)+主动件 kind 词族全量改名不随批(零行为收益)+实机目检归用户。 diff --git a/design/gdd/06-待办清单.md b/design/gdd/06-待办清单.md index b60be9a8..deeddedc 100644 --- a/design/gdd/06-待办清单.md +++ b/design/gdd/06-待办清单.md @@ -18,7 +18,7 @@ **归属**:迭代 17 卡⓪(头卡,用户令插队——①-④卡顺延);任务书=[任务书-内容数据驱动扩展批-2026-10-06](../research/任务书-内容数据驱动扩展批-2026-10-06.md)(T0-T7≈4.5d,零待裁点) -**状态**:已立项(任务书已成文待 U0 用户发令开工) +**状态**:**已实施完成(2026-10-06 当日立项当日落地——worktree iter17-dd 分支待合并)**:T1 框架(效果引擎+16 发射点,commit 7d157b3)→T2 词汇落码(225765f)→T3 专长 27 行迁移+旧挂点枚举消亡(e28a6cb)→T4/T5 秘宝六件+掘进蓄能受理闸+令牌兑付收敛(a33ddaf)→T6 回归闸全绿(冒烟 800 项 0 失败、单元 17 套、探针闸含 click、MC 二档对拍 40 局同种子**逐位一致**——post 锚 `sim-data/runsum_iter17dd_post_{off,formed}_n20_20261006.jsonl` 留盘);出口报告=[iter17-datadrive-exit-report-2026-10-06](../qa/iter17-datadrive-exit-report-2026-10-06.md)。遗留人闸两项随报告呈报(dig_charge 积点模判定留编排器侧=词汇表边界;主动件 kind 词族全量 rename 不随批) #### 菜单开局未传同调装配与局外树规则——疑似缺陷待复验(2026-10-06 登记) diff --git a/design/gdd/systems/冒险循环/奇物.md b/design/gdd/systems/冒险循环/奇物.md index 4ce8891e..7f809c0c 100644 --- a/design/gdd/systems/冒险循环/奇物.md +++ b/design/gdd/systems/冒险循环/奇物.md @@ -29,7 +29,7 @@ | 负责 | 不负责 | |---|---| -| 奇物的定义、三个分类、以及分类与渠道的交叉开关 | 每件效果怎么接入结算——归挂点所属文档([战斗结算](../战斗循环/战斗结算.md)、[行动经济](../排雷循环/行动经济.md)、[地形变异](../排雷循环/地形变异.md)) | +| 奇物的定义、三个分类、以及分类与渠道的交叉开关 | 每件效果怎么接入结算——归订阅事件所属文档([战斗结算](../战斗循环/战斗结算.md)、[行动经济](../排雷循环/行动经济.md)、[地形变异](../排雷循环/地形变异.md)) | | 生命周期边界:何时出现、何时生效、何时清零 | 卡面排版与界面呈现细则——见 [HUD框架](../表现与支撑/HUD框架.md) | | 获取渠道的口径与约束 | 魔法道具与元进度——归上表两份文档 | | 与相邻概念的边界 | 格面资源、关卡节点、商店本身的规则——归各自所属文档 | @@ -61,7 +61,7 @@ | 分类 | 它给的是什么 | 落点 | 事实源 | |---|---|---|---| -| **专长**(Feat) | 给规则。在结算管线某个挂点上给自己加一条规则;同一标签的持有件数达阈值时质变 | 各结算段挂点;标签与档位由求值服务统一算 | `relics.tres`(逐项定义) | +| **专长**(Feat) | 给规则。订阅结算管线的某个事件(订阅哪个事件 / 什么条件 / 什么效果三字段声明);同一标签的持有件数达阈值时质变 | 各结算段事件发射点;标签与档位由求值服务统一算 | `feats.tres`(逐项定义) | | **属性值提升**(ASI) | 给数字。抬高角色某项战斗属性的基础值 | 冒险加算区的生命上限 / AC / 攻击三轴 | `adventure.tres`(种类配额与值) | | **资源** | 给"可用的量"。不抬高战斗属性,也不改规则,而是扩充你能调动的余量与机会 | 行动经济、三选一界面、局内钱包;**不进加算区、多数不进持有集** | `adventure.tres`(种类配额与值) | @@ -272,8 +272,8 @@ |---|---| | [魔法道具](../排雷循环/秘宝.md) | 最需要分清的一对,判据是生命周期:**奇物当局清零,魔法道具跨冒险保留**。两者互不相通——奇物永不进同调位,魔法道具永不进当局随机渠道。 | | [元进度](../跨局循环/元进度.md)(树) | 树管账号永久的规则与容量,奇物管当局构筑,两层互不代替。 | -| [战斗结算](../战斗循环/战斗结算.md) | 专长的挂点位于结算管线的各段;挂点清单与语义归结算文档。 | -| [行动经济](../排雷循环/行动经济.md)、[地形变异](../排雷循环/地形变异.md) | 部分挂点落在行动点结转与地形处理上,语义归各自文档。 | +| [战斗结算](../战斗循环/战斗结算.md) | 专长订阅的事件发射点位于结算管线的各段;事件词汇与发射时机归结算文档。 | +| [行动经济](../排雷循环/行动经济.md)、[地形变异](../排雷循环/地形变异.md) | 部分事件(付费开掘 / 地形受伤)落在行动点收对价与地形处理上,语义归各自文档。 | | [关卡节点地图](关卡节点地图.md) | 是渠道的载体,不是奇物的属性。专长挂在 Boss 节点兑付面上,节点类型词表零扩类。 | | [地图战斗资源](../战斗循环/地图战斗资源.md)(格面资源) | 无关:格面资源是单格的一次性产出,不进持有集,也不是渠道给的。 | | [存档序列化](../表现与支撑/存档序列化.md) | 断点负载记录持有集与剩余候选池;键名与版本迁移策略归存档文档。 | @@ -299,7 +299,7 @@ | 参数 | 取值域 | 事实源 | |---|---|---| -| 专长定义行(标识 / 显示名 / 稀有度 / 标签 / 挂点 / 效果字典) | 各字段封闭枚举,行键白名单封闭 | `game/data/tables/feats.tres` | +| 专长定义行(标识 / 显示名 / 稀有度 / 标签 / 效果说明 / 订阅行〔订阅哪个事件 on / 什么条件 when / 什么效果 do〕) | 各字段封闭枚举,行键白名单封闭;事件 / 效果类型 / 条件事实 / 算子四词表封闭(效果引擎单一事实源) | `game/data/tables/feats.tres` | | 专长门票(出现概率 / 配额 / 全局限量) | 整数 | **尚未入表**,见待办 | | 属性值提升种类 | 封闭三项:生命上限 / AC / 攻击,扩项走文档修订 | `game/data/schemas/adventure_table.gd` | | 资源种类 | 行动点上限 / 金币 / 回复(**刷新次数与专长获取两项尚未入表**,见待办) | 同上 | @@ -345,6 +345,7 @@ | v2.1 | 2026-10-05 | **交叉表收口(用户拍板)**:Boss 渠道**只出专长**,属性值提升与资源在此均不出现——三条渠道的形状定为"一块独占 + 两块并行",专长靠打赢、其余靠抽取与购买。Boss 兑付明确为**终局前**的 Boss,终局 Boss 不给(无后续关卡可消费)。新增 2.4 节**「计价物不自售」规则**:金币不进商店既不拆分类也不开特例,而是写成两轴交叉处的一条约束——商店的计价物是金币,故金币不进商店;按"能不能进商店"反推分类等于用渠道轴去切分类轴,两根轴会缠回去。 | | v2.2 | 2026-10-05 | **门票形态 + 约束分层 + 默认原则**。① 用户明确:奖励节点与商店不直接给专长,而是给一个**「专长获取」道具**,拿到后开专长三选一——交叉表专长在两渠道由"不出现"改为"**门票形式**";门票归专长(给的是一件以前不会的事,只是把选择推迟),理由是防止用钱定点买构筑。② 用户质疑「计价物不自售」本质是特判——**承认成立**:它确实是在分类白名单之外加的一道过滤,与死特判的区别在于把"枚举例外"变成"**可计算谓词**"(新渠道/新奖励自动判定)。2.4 改写为**三层约束**(分类白名单 / 可计算谓词 / 显式例外清单),并给出判断标准:结构性原因可写成谓词,设计性原因只能进清单且每条须写理由;三个方案(单独分类 / 谓词 / 死特判)的代价并列摆出,不藏折中。③ 用户定**默认原则**:除已登记的特殊规则外,分类内的东西都要在池子里——据此**回复应在商店内**,现行 shop.tres 货架缺它是"现状与表格不对齐",须在实施批补上。 | | v2.3 | 2026-10-05 | **门票机制细化 + 默认原则表述修正**。① 门票**拿到即开、不可囤积**(用户定)——不进持有集,防止玩家把关键决策无限推迟,也防持有物语义从后门回来。② 门票的"罕见"**只在概率不在价格**,一局限购一次,并加**计步保底**:连续 5 次三选一展示未出现则第 6 次必出;计数跨奖励节点与商店共享、**刷新后的重新展示计入**(用户明定含刷新),限购用满后保底不再触发。③ **修正 v2.2 的误读**:把"分类在某渠道不出现"当成需要理由的缺格是错的——渠道出哪些分类是渠道轴的定位(Boss 只出专长),不是例外;默认原则只管**分类内部**的成员齐不齐。④ 专长总量与档位阈值的匹配按用户决定**延后**(待第四关内容批后实测复裁),另单独登记 charge 标签仅 3 件、永远达不到档二的数据缺陷。 | +| **v2.8** | 2026-10-06 | **效果行为数据驱动换代(内容数据驱动扩展批)**:专长的效果声明从「挂点+效果字典」换为「订阅行」——每件声明**订阅哪个事件(on)/ 什么条件(when)/ 什么效果(do)**,由效果引擎统一解释执行;旧挂点枚举消亡(事件名+效果类型取代)。配套事实:效果引擎 `game/core/effect_engine.gd`(事件 16 / 效果类型 24 / 条件事实 9 / 算子封闭词表);专长行 `feats.tres` 27 件全量迁移,秘宝被动六件同批订阅化;行为与迁移前逐位一致(MC 同种子 40 局逐位对拍实证)。 | **v2.7** | 2026-10-05 | **三个子节点正式定名(用户拍板:去「类」字后缀)**。此前三个分类只有带后缀的描述(机制类 / 属性提升类 / 资源类),是文档层临时叫法,既非专名也非玩家可读的词。现定专名:**专长**(Feat)/ **属性值提升**(ASI)/ **资源**。定名依据:前两个是 DND 规则书官方中文,且与用户 v1.1 原话一致(当时原话就是「属性提升(ASI)」与「专长」,v1.1 落文时被加了「类」字后缀而退化成描述)。「机制类」这个叫法本身还有硬伤——它的 28 个成员里没有一件是"机制",实际是"在结算挂点上改一条规则",读者看到会误解为游戏机制。 **新增 1.6 节「三层结构与命名」**(用户 2026-10-05 拍板做玩家可见的三层):奇物(总称)/三个分类/具体单件。明确**命名是本节规矩、呈现是另一件事**——分类名在卡面不出现(归 battle.md / node-reward.md 裁决),但帮百科、成就名、效果描述的指代三处都要用,所以它必须是玩家读一遍就懂的正式名词。连带:修掉批量替换撞出的五处「专长(专长)」类自撞、v2.0 历史行内的自撞并标注当时的旧称、registry 的「资源(命名待定)」过期标注。 | **v2.6** | 2026-10-05 | **修正核心循环位置图:奇物不是打一场战斗给一份**(用户纠错)。原图把"关内 排雷推理 → 战斗结算 → 通关 → 得到一份奇物"画成一条链,读起来就是每场战斗通关都给一份——**错在把关内普通战斗的通关与关底 Boss 的通关混为一谈**。按 [冒险结构](冒险结构.md) 的定义,**通关一关 = 该关关底 Boss 被击杀**,所以"击败 Boss"与"通过这一关"是同一个时刻、不是一条链上的两步。改法:① 图改为分支结构(普通战斗节点→不给、往下走;关底 Boss 击败→专长三选一+通关回血);② **一关只兑付一次**写成显式口径;③ 2.1 节渠道名由"终局前 Boss 击败后"改为"关底 Boss 击败(即通过这一关)",并写明触发点就是关卡推进本身那一刻;④ 文首引言加"兑付频率"条,交叉表渠道名与 registry 同步;⑤ 连带修掉一处污染——节点类型封闭枚举五种里的"奖励"曾被 v2.4 批量替换误伤成"奇物"(节点类型里没有奇物这一类),已复原。 | diff --git a/design/gdd/systems/战斗循环/敌人系统.md b/design/gdd/systems/战斗循环/敌人系统.md index 6123e602..898f246c 100644 --- a/design/gdd/systems/战斗循环/敌人系统.md +++ b/design/gdd/systems/战斗循环/敌人系统.md @@ -167,7 +167,7 @@ Boss 专属身份 = 专属池承载:独占技能 + 权重轮廓(**三代错 即:普攻完全不可预测;特殊技有一次类型级预告,不可撤销,但给"继续挖还是停手"的决策依据。 -**半血解锁弹出规格(2026-10-05 技能池执行批,D8 用户裁)**:Boss 血量降至 `unlock_hp_pct` 阈值(池条目字段,域 [1,99],缺省无字段=无门槛)时,下一个开轮摇号前出一次归因明细行「XX 解锁了 YY」(高光键 `skill_unlock`,行型复用 highlight 零新增);呈现层对该行弹**一次性技能名 toast**(轻量提示,非演出——零动画资产、不改演出规格),只报技能名不报数值,池与权重不公开纪律不变。解锁=单向闩(治疗抬血不解锁);锁定条目双面过滤(不进可摇集合与保底特殊技集合——保底不得逼出未解锁技);解锁后冷却初值 0 即可摇。投放纪律=字段通用、仅 Boss 行投放(enemy 表无 boss 编码位,硬闸须跨表查节点类型——不做硬闸,本句即文档纪律登记)。旧环"下一拍全程亮出(意图徽标 + 环点)"的预告粒度随行为环退役。技能动画的攻/防系映射归[战斗表现](战斗表现.md),敌人侧摇定动画位归[HUD 框架](../表现与支撑/HUD框架.md)(呈现载体 = 站桩立绘,2026-09-20 全站桩拍板),本系统只锁上面这张三档规格。 +**半血解锁弹出规格(2026-10-05 技能池执行批,D8 用户裁;内容数据驱动扩展批 T1 补登记——解锁判定点=通用事件「血量阈值 `enemy_hp_threshold`」的发射点,对所有带 `unlock_hp_pct` 字段的敌人逐个发射、非 Boss 专属——Boss 语义留给订阅条件判定)**:Boss 血量降至 `unlock_hp_pct` 阈值(池条目字段,域 [1,99],缺省无字段=无门槛)时,下一个开轮摇号前出一次归因明细行「XX 解锁了 YY」(高光键 `skill_unlock`,行型复用 highlight 零新增);呈现层对该行弹**一次性技能名 toast**(轻量提示,非演出——零动画资产、不改演出规格),只报技能名不报数值,池与权重不公开纪律不变。解锁=单向闩(治疗抬血不解锁);锁定条目双面过滤(不进可摇集合与保底特殊技集合——保底不得逼出未解锁技);解锁后冷却初值 0 即可摇。投放纪律=字段通用、仅 Boss 行投放(enemy 表无 boss 编码位,硬闸须跨表查节点类型——不做硬闸,本句即文档纪律登记)。旧环"下一拍全程亮出(意图徽标 + 环点)"的预告粒度随行为环退役。技能动画的攻/防系映射归[战斗表现](战斗表现.md),敌人侧摇定动画位归[HUD 框架](../表现与支撑/HUD框架.md)(呈现载体 = 站桩立绘,2026-09-20 全站桩拍板),本系统只锁上面这张三档规格。 | 规格 | 含义 | |---|---| @@ -297,4 +297,5 @@ Boss 专属身份 = 专属池承载:独占技能 + 权重轮廓(**三代错 | v0.10 | 2026-09-19 | **技能池新效应词汇批(用户拍板——设计权威 proposal-status-vocabulary.md〔提案已吸收退役 git a6833b6a——现行事实源=敌人系统.md 词汇表/增益状态.md〕 v1.0,与 战斗结算 / 增益状态 / registry / 切片代码同批)**:① 技能类型枚举三→四(+妨害 debuff——对主角施减益的 0 伤害施放动作,字段复用 buff_ref/buff_duration,落点由状态行 target_side 路由〔增益 → 敌方侧 / 妨害 → 主角侧,施放类 side 守卫加载拒绝〕,执行与增益技同族施放行语义——不掷不折不兑付伏击);② 伤害技新字段 rider_mult(必中附加分量——"固定值伤害"改形:固定值不随基线缩放与共享技能表前提冲突,必中小倍率承载"躲不掉的稳定伤害");③ 池构成纪律 +每池妨害 ≤1;④ 意图三档特殊技预告扩三族(攻击系 ⚔ / 增益系 ✦ / 妨害系 ☠);⑤ 三词收敛两词——多段命中经用户拍板退役(无防御磨损体系,无对照机制),P1 字段位「命中段数」保留;⑥ 实装面 = 仅 L2 Boss 蛛化卓尔精灵(venom_burst + rider_mult 0.75;新行 web_snare 蛛网束缚 debuff→off_guard,首发载荷 = 失衡〔用户拍板〕,乏力 sapped 留作第二词汇),其余敌种不动 | | v0.11 | 2026-09-20 | **伏击单轮上限删除批(用户拍板——与 战斗结算 / 地图战斗资源 / 难度曲线与Boss / registry / 切片代码同批)**:① 伏击节上限段与溢出可见行删除,一轮踩雷数由行动点预算天然封顶成文;② 字段表 advantage_per_round_cap 行删除(原 0 = 免疫伏击语义作废);③ 结构冻结联动纪律去单轮上限 × 攻击基线、数量 × 上限两条;④ 验收标准伏击授予与表校验条同步 | | v0.12 | 2026-09-20 | **格面资源八型批(用户拍板——与战斗结算 / 地图战斗资源 / 行动经济 / registry / 切片代码同批)**:① 伏击节整体退役改写(复利集中单血条与群伤奖励双重不对称,用户裁决);② 责任表/结构冻结/验收标准的伏击词条目拆除;③ 群伤负向现况 = 警觉(敌方护甲等级 +1),定义与承载移交增益状态 | +| v0.14 | 2026-10-06 | **事件发射点登记(内容数据驱动扩展批)**:半血解锁判定点升级为通用事件 `enemy_hp_threshold` 发射点(对所有带 unlock_hp_pct 字段的敌人逐个发射——非 Boss 专属,Boss 语义留给订阅者条件判定;本批零订阅者,登记归效果引擎事件词汇 16 发射点之一,后续内容批随批接线)。零行为变化。 | v0.13 | 2026-10-05 | **技能池执行批(迭代14④——D1-D8 全裁零待裁,与 skills.tres/enemy.tres/registry 同批)**:①池构成表 8 敌种全覆盖(补 L2+L3 五敌种行:巨蜘蛛/蛛化卓尔精灵/灰矮人/钩爪恐魔/石君王——池构成轮廓+定位列,权重数值不登)+护甲列改读表口径(失同步欠账清偿);②Boss 句改三代错峰口径;③意图呈现节补半血解锁弹出规格句(**Boss 半血解锁新独占伤害技**:字段 unlock_hp_pct〔1,99] 缺省无=无门槛/单向闩/双面过滤/保底不逼锁定/解锁出一次归因行+一次性技能名 toast——D8 只报名不报数值零演出;三 Boss=震地怒击 ×2.5/CD4·遏毒 ×2.0/CD3·岩崩 ×2.5/CD4 案甲 provisional 归调参);④首拍加权取消〔D2〕杂鱼纹理注销不立文 | diff --git a/design/gdd/systems/排雷循环/秘宝.md b/design/gdd/systems/排雷循环/秘宝.md index 40628304..23fa0b63 100644 --- a/design/gdd/systems/排雷循环/秘宝.md +++ b/design/gdd/systems/排雷循环/秘宝.md @@ -176,6 +176,8 @@ 结构冻结(非调参项,改动走本文修订流程):真视窄域护栏、零信息目标不消耗、边缘窗口照常受理、使用零行动点成本、库存关卡域、用后不可逆、零随机。 +效果声明形态(内容数据驱动扩展批换代,2026-10-06):被动件的效果行为从按件 id 硬编码换为**订阅行**——行内声明**订阅哪个事件(on)/ 什么条件(when)/ 什么效果(do)**,由效果引擎统一解释执行(淬毒刃纹=克制判定事件、标中殉爆=标旗事件、惊巢反制=踩雷事件、工兵晨令=每轮开始事件、偏运硬币=资源掷定事件);主动件保持按效果类型通用分发(掘进蓄能的受理闸=使用事件订阅+积点计数器账本)。事件 / 效果类型 / 条件事实 / 算子四词表封闭(效果引擎单一事实源),卡面效果摘要=行 `desc` 字段直读(文案表值化)。 + playtest 预警(预注册,归难度曲线与Boss): | 预警 | 指标与读法 | @@ -214,4 +216,5 @@ playtest 预警(预注册,归难度曲线与Boss): | v0.6 | 2026-09-30 | **拍板点 1 收口批(2026-09-30 用户裁决甲:同调=终局方向)**:①新增「定位与形态沿革」节——现行正文定性=切片期过渡形态、终局=局外同调件(设计权威底稿=原提案八拍板,git 57da58c 已退役),两态关系=形态替换、衔接纪律与事故留痕入节;②正文口径回写四处:五维度表信息/规划/反制三行改现行态(罗盘退役/显影件封存/豁免面入正文)、覆盖检查改投放维度覆盖口径、掉落池获取节补解锁门控(2026-09-29 拍板③甲)、板级叠加层预读集改「机制在案无投放来源」;③验收两条(护栏/覆盖检查)与调参表投放态注记、地形变异依赖行同步;④实施批登记见 06「秘宝同调实施批」条。审计冲突 C19/C29 随批收口(全录见审计报告 git 提交 c095916,报告已退役) | | v0.7 | 2026-10-01 | **奖励节点渠道改写注记(迭代 10 追加批——用户拍板「奖励节点与通关三选一共享同池」)**:①「奖励节点三选一构筑点」形态(v0.5 ④)退役——奖励节点改提升三选一共享池(秘宝件为池中 1/6 权重选项,同调位满位替换交互随 BonusPickOverlay 共用面生效,spec:node-reward.md v2.0);②道具件经节点渠道入本关库存(来源归因 reward_node,与掉落/三选一渠道同受 transitional_sync_slots 过渡上限约束);③战斗掉落渠道零改动;④本批过渡约束形态与切换义务注记见 registry aux_items 条同日批注;同调实施批的全渠道统一收口义务不变 | | v0.8 | 2026-10-03 | **反制契约作废批(2026-10-03 用户终裁——层级错配失败设计)**:①五维度「反制」行与豁免面 {plan, info, counter} 的 counter 注记改终裁态(原框架契约「每变异≥1反制道具」作废,终裁理由见地形变异.md v0.6/本文件 v0.8 版本行(原 06 条目已收口移除));②上游地形变异依赖行、P2 后置行同步改「迭代 12 调研前置重定」;③地形变异.md v0.6 同批落义务作废注记;④迭代 11 范围裁定(同调→商店→迷雾三批)见 roadmap 迭代 11+ 行——反制批移迭代 12 | +| v1.0 | 2026-10-06 | **效果声明换代(内容数据驱动扩展批)**:被动六件(淬毒刃纹/偏运硬币/工兵晨令/惊巢反制/标中殉爆)改订阅行+掘进蓄能受理闸数据化(使用事件订阅+积点计数器)——按件 id 硬编码清零;结构冻结六条零触碰,行为与迁移前逐位一致(MC 同种子 40 局对拍实证)。 | v0.9 | 2026-10-05 | **消歧更名批(用户拍板 2026-10-05):系统更名「魔法道具」→「秘宝」**。**更名理由**:「魔法」一词在本作已被征用为敌方侧伤害/护甲语汇(`magic_bolt 魔法矢` / `magic_recoil 魔法反噬` / `magic 魔法族` 格面微光 / `ward 魔法护甲`),「魔法道具」易被读成"魔法属性的道具";「秘宝」取"神秘·强大·稀有"义,与九件内容气质(工兵/工程/手艺向)契合,且与局内道具(奇物)不冲突。**同调位机制名保留不变**(attunement 仍为 DND 词,机制血统不受更名影响)。**本批仅改文档称谓,代码不动**——代码标识符仍为 `aux_*` / `AUXILIARY_ITEMS`(auxiliary 词根为过渡形态遗留,与中文名不同步属已知欠债,另批处理);退役路径残留玩家可见文案(`battle_narrator.gd:117-118`、`bonus_interact_controller.gd:76`)亦留另批。历史定稿文档(research/ 提案与任务书、qa/ 检查单、archive/ 归档)按换名先例原样保留不改。 | diff --git a/design/qa/iter17-datadrive-exit-report-2026-10-06.md b/design/qa/iter17-datadrive-exit-report-2026-10-06.md new file mode 100644 index 00000000..da256a4e --- /dev/null +++ b/design/qa/iter17-datadrive-exit-report-2026-10-06.md @@ -0,0 +1,70 @@ +# 出口报告——内容数据驱动扩展批(迭代 17 卡⓪,2026-10-06) + +状态:**批内全部提交完成、六闸全绿、终态=worktree(iter17-dd-20261006 分支)全部提交,待用户合并落 master**。 +执行事实源:[任务书-内容数据驱动扩展批-2026-10-06](../research/任务书-内容数据驱动扩展批-2026-10-06.md)(v1.1);设计权威:[研究-内容数据驱动扩展架构-2026-10-06](../research/研究-内容数据驱动扩展架构-2026-10-06.md)。 +执行形态:前任 subagent 完成 T0-T2 后因额度中断,续跑 subagent 接手 T3 断点(三个脏文件半成品)收口并完成 T4-T7。 + +## 一、提交清单(worktree 内全部提交,按卡) + +| 卡 | 提交 | 内容 | +|---|---|---| +| T1(前任) | `7d157b3` | 效果引擎框架:事件发布订阅解释器+16 发射点埋点+组合规则六族+计数器账本 | +| T2(前任) | `225765f` | 词汇表落码:feats/treasure 两表 trigger 订阅形态 schema 换代+护栏迁移 | +| T3 | `e28a6cb` | 专长迁移:27 行 triggers 全量生效+旧 23 挂点枚举消亡+_feat_flags_for 退役+消费点拆除(伤害控制器 13 处+编排器全部旗面)+文案入表(专长卡 desc)+断言面随迁(冒烟 ④-⑧ 全量改写) | +| T4+T5 | `a33ddaf` | 秘宝六件迁移(淬毒/偏运/晨令/惊巢/殉爆/掘进蓄能受理闸)+按 id 特判清零+pick_refresh kind 分发+curio 卡片文案入表;令牌兑付 match 收敛进效果类型路由表+树规则键同面收口 | +| T7 | 本批终提交 | 文档正主换代(奇物.md v2.8/秘宝.md v1.0/敌人系统.md v0.14+registry 两件+06 状态收口+roadmap 卡⓪行)+本报告 | + +T0 断点核查记录:前任已留 pre 锚(见 §三),T0 三查其余项随续跑开工复读完成。 + +## 二、闸读数(T6 回归闸全绿) + +| 闸 | 读数 | 结论 | +|---|---|---| +| 冒烟(三闸) | **800 项检查,0 项失败**,退出码 0,无 SCRIPT ERROR | 绿(断言数 783→800:T3 断言面随迁净增 17 项,漂移追认随 T3 commit 留痕) | +| 单元(run_all) | **17 套全绿** | 绿 | +| 探针闸(probe_gate --click) | ui_input/ui_struct/ui_contrast/click 四段全 PASS,批末残留 Godot=0 | 绿(本批触碰呈现层——battle_narrator/curio_interact 文案面,故带 click) | +| MC 二档对拍 | 见 §三 | 绿——逐位一致 | +| 确定性复放 | 同种子改前/改后两次采集逐位一致(40 局全对) | 绿(强于「同码两遍」口径) | +| doc 三闸 | doc_link_audit 过(exit 0);doc_term_audit 过(exit 0);gdd_lint 2 处失败=**基线既有**(06-待办清单/07-体验验证清单结构性指标,stash 对比实证改前改后同为 2 处零新增) | 绿(零新增) | +| 熵增闸(repo_structure_audit) | 1 处违例=design/diagrams 子目录越界——**基线既有**(前批「GDD系统架构总图批」00712d5 产物,非本批引入;归该批主自证或后续清理) | 绿(零新增) | + +## 三、MC 锚与对拍结论(二档合规) + +- 判据口径:契约 2 两套(确定性复放与冒烟=**逐位一致**;MC 统计量=**成型画像同带**,正主=design/qa/m4iter12-t7-batch1-readings-2026-10-04.md)。 +- 改前锚(前任 T0 采集,留盘未补跑):`sim-data/runsum_iter17dd_pre_off_n20_20261006.jsonl` / `runsum_iter17dd_pre_formed_n20_20261006.jsonl`(seed0=83000、N=20/档、normal 档、route=min、attune=off、off/formed 同种子配对;formed=七流派各 2 件档一成型注入画像——14 件全部系本批迁移行)。 +- 改后锚(T6 采集):`sim-data/runsum_iter17dd_post_off_n20_20261006.jsonl` / `runsum_iter17dd_post_formed_n20_20261006.jsonl`(同 seed0 同口径;新锚去前缀纯 JSONL 按种子升序)。 +- **结论:40 局(2×20)同种子逐位一致**——每局 RUNSUM 全字段(通关数/承伤/踩雷/旗经济/资源事件/钥匙石/逐战上限序列等 33 字段)改前改后零差。统计量同带判据自动满足(逐位一致蕴含同带):L1 通关率 off 20%(4/20)、formed 80%(16/20),与改前逐位同值。 +- 档位合规:总量 40(每档 20)=二档上限贴边;另有 T3 段末 S1 预检 2×5 局(三档只测不判,未留锚)——S1 停机点未触发。 + +## 四、代拍板记录(任务书 §八白名单四项+呈报不停机级,全部 commit 留痕) + +1. **fact 查询面落法**:`has` 算子=在场判定(value 缺省=非空;value 给定=数组包含)——地形伤/伤害型地形四行订阅的查询面(T3 commit)。 +2. **fact 查询面落法**:perfect_defense「全中」装不进封闭 9 项事实——when 落 flags_count≥1 保守判定,全中闸=flag_settle 发射点施加面承载(禁私开 fact 停机点纪律保持;T3 commit)。 +3. **事件载荷字段**:attack_commit 发射点从 resolve 内移至开单入口(任务书 §3.1 定稿位),命中随单据 context 携带;damage_taken 补 terrain_kind、flag_hit 改 terrain_kind=邻接伤害型变异集(T3/T4 commit)。 +4. **发射点微调(±1)**:round_start 发射位前移至行动经济轮切换之前(树规则 grant_ap queue 当轮兑现=先手压制首轮语义锚);attack_commit 单发射点、命中两处消费(编排器段序承载连珠矢开单位/控制器管线算术承载其余施加面——dispatch 纯读零副作用)。 +5. **效果类型落法对拍注记(映射表五处语义修正的随批追加,行为等价优先)**:猎巢人/矿工的执拗=必中附加通道(映射表标 modify_ticket/attack_bonus 与现状不符,落 add_ticket_rider per 载荷);整备军备双效果两行订阅;厚积薄发=满层持续状态(非沿触发);荆棘编织者=伤害兑付位第二份产出(非 ×2 乘区);完美布防=旗伤单值倍增(非 yield 乘区)——全部随 .tres 尾注+commit 留痕。 +6. **冒烟断言数漂移追认**:783→800(断言面随迁净增 17 项)。 +7. **文案微调**:掘进蓄能/失义件秘宝卡补实际摘要(原 kind 硬编码面无文案);其余 desc 措辞与旧映射逐字一致。 +8. **效果类型计数**:24 型(任务书标题 23 系笔误——T1 前任已代拍留痕,沿用)。 + +## 五、文档正主换代清单(T7) + +- 奇物.md **v2.8**:专长效果声明换代(挂点→订阅行)五处+版本行;事实源表 feats.tres 行结构换代。 +- 秘宝.md **v1.0**:效果声明形态节(被动件订阅行+主动件 kind 分发+掘进蓄能受理闸数据化+desc 文案表值化)+版本行。 +- 敌人系统.md **v0.14**:半血解锁判定点=通用事件 enemy_hp_threshold 发射点登记(非 Boss 专属,零行为变化)+版本行。 +- registry/entities.yaml:专长 data_sources 换代+effect_declaration 新条(四词表封闭/组合规则/遍历序/重建时机)。 +- registry/architecture.yaml:core_decisions 新条 effect_engine_interpreter(ADR-0011 候选)。 +- 06-待办清单:数据驱动批状态=已实施完成收口(提交号+闸读数+遗留两项)。 +- roadmap:迭代 17 卡⓪行换代(状态=已实施完成)。 + +## 六、遗留人闸项(呈报不停机级,均为显式拍板请求非缺陷) + +1. **掘进蓄能积点模判定留编排器侧**:use_treasure 受理闸已数据化(treasure_used counter_spend 订阅),但「每 charge_n 安全格积 1 点」的模判定留在战斗编排器授予段——「先增后判」在发射点施加架构下无法行内表达(when 于 dispatch 预评估,先增后判需引擎内联过账=会破坏矿脉直觉连击的施加序等价)。行为与迁移前逐位一致(charge_n 读行零字面量);若要求彻底行内化,需效果引擎增设「计数器种子/内联过账」机制,归后续词汇表扩展批裁决。 +2. **主动件 kind 词族全量 rename 不随批**:新词(reveal_tiles/preview/grant_ap/remove_mines/refresh_pick)已在闭集与旧词共存(T2),本批只完成 pick_refresh kind 分发合流;mine_reveal→reveal_tiles 等全词族 rename 波及库存/呈现标记/单测约 15 处零行为收益,显式不在本批(出口呈报,归后续清理批或随批裁决)。 +3. **实机目检归用户**(批次工作流第 12 步):呈现面改动=专长卡与秘宝卡文案源(desc 直读,措辞逐字迁移)——建议实机抽查一张专长卡+一张秘宝卡。 + +## 七、风险与边界声明 + +- 存档零触碰:存档键/格式/断点结构一字未改(持有集/装备存 id 不变,换的只是解释方式)。 +- 护栏全保留:四词表封闭校验、行键白名单、悬空引用致命、零裸 flat 禁令、流派扫雷触发铁律(MINE_EVENTS 同义形态)全部以新形态延续;本批新增封闭面=when value_from 悬空校验。 +- 主区零直改:全部改动在 worktree iter17-dd-20261006;并行在飞三线(iter17-pause 卡③/standee-tier/uialign)未触碰。 diff --git a/design/registry/architecture.yaml b/design/registry/architecture.yaml index 6bfc7eb7..ad30407a 100644 --- a/design/registry/architecture.yaml +++ b/design/registry/architecture.yaml @@ -39,6 +39,12 @@ api_decisions: adr: ADR-0005 decision: SCHEMA_VERSION 比较逻辑归 存档序列化(存档版本 > 当前 = 拒读);RNG 快照 = master_seed + 各流 scope 计数器 + 意图/窗口选择序列,切出须在 scope 事件边界外(无在飞 open_stream 实例——战斗结算 编排保证) +core_decisions: + - name: effect_engine_interpreter + adr: ADR-0011(候选——内容数据驱动扩展批 2026-10-06) + decision: 内容效果行为=发布订阅解释器(EffectEngine)——结算管线同步 emit 事件(无队列无异步),内容行以「订阅哪个事件 on / 什么条件 when / 什么效果 do」声明订阅,解释器遍历持有订阅集返回按声明序排列的命中,发射点按组合规则施加;旧 23 挂点枚举与按件 id 特判消亡 + note: 事件/效果类型/条件事实/算子四词表封闭于 EffectEngine 常量(单一事实源,schema 校验引用);遍历序=数据表声明序写死保种子复放;订阅重建=每战 begin_battle(中途获得下一战生效);不做通用拦截器链,改写窗口=判定点就地按声明序施加(R2 裁决) + forbidden_patterns: - pattern: autoload_global_event_bus adr: ADR-0001 diff --git a/design/registry/entities.yaml b/design/registry/entities.yaml index ea6ff450..e3cd448a 100644 --- a/design/registry/entities.yaml +++ b/design/registry/entities.yaml @@ -600,7 +600,8 @@ entities: lifecycle: 候选池构建(冒险开局一次)→ 在渠道上出现(三选一形式)→ 选定(自池中剔除不再复现;选到「专长获取」时立刻再开一次专长 N 选一)→ 持有生效或即时兑付 → 冒险以任一结局收束时整集清零;断点记录持有集与剩余候选池 acquisition: 三条渠道的**目标分工(交叉开关,v2.2 收口)**——专长:主渠道=**关底 Boss 击败=通过这一关**(**唯一直接给的渠道**;不新增节点类型、挂 Boss 节点兑付面;末关的 Boss 击败后不给——无后续关卡可消费)。**兑付频率**:奇物不是打一场战斗给一份,关内普通战斗节点不给任何奇物,一关之内只兑付一次(奖励节点与商店同理,各节点各自兑付),奖励节点与商店**只给「专长获取」门票**——**拿到即开、不可囤积**,不进持有集(选中/买下同一时刻开专长三选一;可囤积会让玩家无限推迟关键决策,且把持有物语义从后门放回)。门票归专长(给的是一件以前不会的事,只是把选择推迟)。罕见**只在概率不在价格**,一局限购一次,**计步保底**:连续 5 次三选一展示未出现→第 6 次必出(计数=展示次数、刷新重展计入、奖励节点与商店共享、触发清零、限购用满后不再触发);属性值提升:奖励节点 + 商店(口径「训练」);资源:奖励节点 + 商店(**金币除外、回复在内**);后两类均不出现在 Boss 渠道。形状=「一块独占 + 两块并行」,专长靠打赢、其余靠抽取与购买。**默认原则**:除已登记的特殊规则外,分类内的东西都要在它该出现的渠道池子里——任何不出现都须登记理由**现行实现三渠道共用同一候选池、三类混编**,分工尚未实施,登记于 06-待办清单 BD 分层条目 boundary: 判据走生命周期——**一次冒险内获得、冒险收束即清零的是奇物;跨冒险跟着你走的是魔法道具**。奇物永不进同调位,魔法道具永不进当局随机渠道;与元进度天赋树两层互不代位 - data_sources: feats.tres(专长定义行)/ adventure.tres(属性值提升与资源的种类配额与值)/ shop.tres(货架取舍) + data_sources: feats.tres(专长定义行——效果声明=triggers 订阅行〔订阅哪个事件 on / 什么条件 when / 什么效果 do〕,旧挂点+效果字典形态 2026-10-06 内容数据驱动扩展批退役)/ adventure.tres(属性值提升与资源的种类配额与值)/ shop.tres(货架取舍) + effect_declaration: **效果行为=事件订阅(2026-10-06 内容数据驱动扩展批换代)**——事件/效果类型/条件事实/算子四词表封闭于效果引擎 `game/core/effect_engine.gd`(事件 16 发射点 / 效果类型 24 / 条件事实 9 / 算子封闭;四词表出域=加载期校验致命);组合规则六族(加法/乘法/任意真/按序改写/按序消耗/即时执行)为效果类型必填属性;遍历序=数据表声明序写死保种子复放;订阅重建=每战开始从持有集/装备集重建(中途获得下一战生效) charge_defect: **charge 档二不可达登记(T7 奇物批迭代15②——数据缺陷留痕,白名单4授权「只登记不硬补」)**:feats.tres 现行 charge 标签仅 3 件(<4),档二激活要求同标签 ≥4 件——charge 流派永远达不到档二,即使获取次数无限。补哪几件系内容裁决,转独立内容批(硬补占位件已否决);档二可达性复裁=第四关内容批落地后实测(C2 待裁点)。 pool_architecture: **池架构=总池-渠道子集(多级缓存形,用户 2026-10-05 裁 T6 时确立)**:总池=单一令牌库(TIER_QUOTAS 70/25/3=98 + 专长活行追加);三渠道按分类白名单从总池取**子集视图**(关底 Boss 击败=只出专长子集;奖励节点/商店=属性值提升+资源子集,专长只以门票形态出现)——渠道不各建物理池,白名单在候选视图层生效(T4 过滤式)。配额挤占重裁:heal rare 5→3、refresh rare 0→2(方案B,总量 98 锚不变;用户裁「渠道从总量里拿子集」时同步裁)。 refresh_boundary: **刷新次数边界写死(T6 奇物批迭代15②——防后续混淆结算)**:「商店独立刷新机制」(shop_refresh_free/shop_refresh_cost_base/shop_refresh_cost_curve 三旋钮——进店后的免费次数+GP 付费刷新)系**商店自身服务**,作用对象=货架重抽;奇物 refresh 件(资源类成员,作为奇物授予的重抽次数)作用对象=**三选一界面**(兑付即 _attune_refresh_x 增量,X 余量=三选一重抽次数),不进加算区(§2.2 加算区四轴不含它)。两者不是一回事,禁止混算。 diff --git a/game/core/action_economy.gd b/game/core/action_economy.gd index bceccfac..5b00fc60 100644 --- a/game/core/action_economy.gd +++ b/game/core/action_economy.gd @@ -35,19 +35,19 @@ var last_round_end: Dictionary: var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start var _next_round_mode := "" # 调试观测量(同板续轮 / 换新图) var _pending_ap_delta := 0 # 下轮行动点修正排队(格面资源八型批 2026-09-20——action 型 ±1 排队,轮开始兑现后清零) -# 蓄力流运行态(迭代 12 T5——relic 件注入面;false=关着零扰动): -# feat_ap_carry=整备军备(轮末剩余 AP 转蓄力层替代 RAP 转存——cleared 作废口径改写的停手侧承载); -# feat_dig_extra=深挖钻头付费开掘对价(R4 甲非信息税形态;2026-10-04 试玩缺陷修复——对价挂付费 -# 开掘侧:AP 富余时挖掘付全价 1+extra 积付费计数、AP 不足时降级只付基础 1 照常开格不积计数—— -# 原「持有期所有挖掘无条件 +1」实现把对价错挂成普通挖掘税,AP=1 锁死开格);charge_layers=蓄力层账本。 -var feat_ap_carry := false -var feat_dig_extra := 0 +# 蓄力流运行态(迭代 12 T5 → 内容数据驱动扩展批 T3 换代:旧 feat_ap_carry/feat_dig_extra 注入旗 +# 退役——转蓄力=round_end grant_ap(mode:transfer) 订阅(finish_round_stop 定夺)、深挖对价=dig_paid +# counter_spend 订阅(request_dig 施加面);charge_layers=蓄力层账本) var _charge_layers := 0 var charge_layers: int: get: return _charge_layers -var _deep_paid_count := 0 -var deep_paid_count: int: - get: return _deep_paid_count +# 效果引擎(内容数据驱动扩展批 T1——dig_paid/charge_full 发射面;round_end 转存决策面) +var _fx: EffectEngine = null +var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策移至 round_end 发射点——grant_ap(transfer) 订阅定夺) + + +func set_fx(fx: EffectEngine) -> void: + _fx = fx func _init(db_ref: ConfigDb) -> void: @@ -87,17 +87,25 @@ func try_spend(action_kind: String) -> bool: # 排雷玩法 挖掘专用:chain_size = 连锁附加格数(不含首格);clears_map = 本次动作后全图安全格开完。 -# 深挖钻头付费开掘(2026-10-04 缺陷修复——对价挂付费侧):AP 富余(≥基础+extra)时本动作付全价 -# 并积付费开掘计数(orchestrator 深层产出配额取数面);AP 不足全价时**降级只付基础 1 照常开格**、 -# 不积计数——持有钻头永不锁死普通挖格(散件保底 B-2)。 +# 深挖钻头付费开掘(内容数据驱动扩展批 T3——dig_paid counter_spend 订阅施加面,2026-10-04 缺陷 +# 修复口径不变):按序消耗族——AP 富余(≥基础+对价)时本动作付全价并积本轮付费计数(效果引擎 +# 计数器=授予段追加产出配额取数面);AP 不足全价时 fallback 降级只付基础 1 照常开格、不积计数 +# ——持有钻头永不锁死普通挖格(散件保底 B-2)。 func request_dig(chain_size: int, clears_map: bool) -> bool: if _phase != Phase.ACTIVE: _reject("phase") return false var cost := _cost("dig") - if feat_dig_extra > 0 and _ap >= cost + feat_dig_extra: - cost += feat_dig_extra - _deep_paid_count += 1 + # dig_paid 发射+施加(内容数据驱动扩展批 T3——付费开掘发起、动作经济闸收对价前: + # counter_spend 命中=尝试加收对价并积计数;资源不足走 fallback/耗尽即止=只付常规基础对价) + if _fx != null: + for h in _fx.dispatch("dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}): + if String(h.get("kind", "")) != "counter_spend": + continue + var extra := int(h.get("value", 1)) + if _ap >= cost + extra: + cost += extra + _fx.counter_add(String(h.get("counter_id", "")), 1) if _ap < cost: _reject("resource_face") return false @@ -137,21 +145,35 @@ func _spend_and_arbitrate(action_kind: String, clears_map: bool) -> bool: # 停手:剩余 AP 1:1 转存 RAP;行动经济 直发 stopped 路径 round_end(双发射源)。 # 整备军备改写(迭代 12 T5——feat_ap_carry=true 时剩余 AP 转蓄力层〔上限 3〕替代 RAP 转存: -# 「不动也是行动」——cleared 作废口径改写的停手侧承载;cleared 分支 AP 恒清零维持既有作废语义) +# 「不动也是行动」——cleared 作废口径改写的停手侧承载;cleared 分支 AP 恒清零维持既有作废语义)。 +# 内容数据驱动扩展批 T1:本方法改记账制——剩余 AP 存 _last_stop_ap 后清零,转存决策移至 +# round_end 发射点 finish_round_stop(grant_ap(mode:transfer) 订阅定夺;双轨期旧旗 feat_ap_carry +# 并行生效,行为与迁移前逐位一致)。 func request_stop() -> bool: if _phase != Phase.ACTIVE: _reject("phase") return false - if feat_ap_carry: - _charge_layers = mini(3, _charge_layers + _ap) - else: - _rap += _ap + _last_stop_ap = _ap _ap = 0 _finish_round("stopped", "same_board") round_end_stopped.emit({"path_tag": "stopped", "continuation_hint": "same_board"}) return true +# 轮末停手转存决策(round_end 发射点调用——编排器授予段尾):transfer=true=剩余 AP 转蓄力层 +# (上限 3,整备军备面);false=1:1 转存 RAP(缺省)。非停手轮(cleared/耗尽)_last_stop_ap=0 +# 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。 +func finish_round_stop(transfer: bool) -> void: + if transfer: + var prev_layers := _charge_layers + _charge_layers = mini(3, _charge_layers + _last_stop_ap) + if _fx != null and _charge_layers >= 3 and prev_layers < 3: + _fx.dispatch("charge_full", {"layers": _charge_layers}) + else: + _rap += _last_stop_ap + _last_stop_ap = 0 + + # 轮内刷新(2026-09-12 方案 A 用户拍板;等待期通道 2026-09-15 用户拍板整体移除):唯一入口 = # Active 相位——仅扣 RAP,相位与 AP 均不动。刷新不打断轮次、不触发结算;换图编排归 战斗结算 # (retire + install 立即执行,玩家同轮继续挖新图)。决策主体 = 玩家(经 HUD框架发起)纪律不变。 diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index e312ea57..6f214914 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -425,19 +425,15 @@ func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = _feats.append(feat_id) _curio_taken["relic"] = int(_curio_taken.get("relic", 0)) + 1 return true + # 加算形态路由(内容数据驱动扩展批 T5——match 收敛进效果类型:属性值提升四类走 + # adventure_table.CURIO_APPLY_ROUTES 声明路由(bonus 落点键),加新加算类令牌零代码) + var sink := db.adventure.curio_apply_route(kind) + if sink != "": + bonus[sink] = int(bonus.get(sink, 0)) + value + _curio_taken[kind] = int(_curio_taken.get(kind, 0)) + 1 + return true + # 通道面(副作用形态——显式分支) match kind: - "attack": - bonus["atk"] = int(bonus.get("atk", 0)) + value - _curio_taken["attack"] = int(_curio_taken.get("attack", 0)) + 1 - "max_hp": - bonus["max_hp"] = int(bonus.get("max_hp", 0)) + value - _curio_taken["max_hp"] = int(_curio_taken.get("max_hp", 0)) + 1 - "armor": - bonus["armor_class"] = int(bonus.get("armor_class", 0)) + value - _curio_taken["armor"] = int(_curio_taken.get("armor", 0)) + 1 - "ap": - bonus["ap"] = int(bonus.get("ap", 0)) + value - _curio_taken["ap"] = int(_curio_taken.get("ap", 0)) + 1 "gp": if _runner != null: _runner.grant_curio_gp(value) @@ -649,7 +645,14 @@ func _seed_attune_charges() -> void: # 达上限剔牌/掷定前置直显;停点可中断)---- func _attune_refresh_x_total() -> int: - return int(db.treasure_item_row("pick_refresh").get("magnitude", 0)) if _attunement.has("pick_refresh") else 0 + # 内容数据驱动扩展批 T4——按 id 特判拆除:kind 分发查询(装配集内 effect_kinds 含 bonus_refresh + # 的行 magnitude 合计;刷新件类加件零 id 分支) + var total := 0 + for iid_v in _attunement: + var arow: Dictionary = db.treasure_item_row(String(iid_v)) + if (arow.get("effect_kinds", []) as Array).has("bonus_refresh"): + total += int(arow.get("magnitude", 0)) + return total func _reset_refresh_x() -> void: diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 740830d4..e8415804 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -75,113 +75,65 @@ var _flag_consumed := {} # 兑付资格以雷格实例为键 # 同调装配运行态(迭代 11 第①批 T5——begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动) var attunement: Array = [] # 装备中同调件 item_id 集 var _attune_charges_ref: Dictionary = {} # 充能账本共享引用(迭代 11 第①批 T6——use_treasure 消费直改) -var _attune_bank := 0 # 掘进蓄能积点(battle 域——每 charge_n 安全格 +1 点) -var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数 -# 特性运行态(迭代 12 T2——relic 持有集经 enc.feats 注入;求值旗注 damage_controller.feat_flags; -# 魔矢每战首次计数 battle 域。空集=零扰动) +var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数(进度呈现取数面;积点账本=效果引擎计数器 dig_charge_bank——行 triggers counter_spend 订阅同账本) +# 特性运行态(内容数据驱动扩展批 T3——relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经 +# 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动) var _feat_first_dig_used := false -var _feat_combo := 0 # 矿脉直觉连击计数(battle 域——safe +1/mine 清零) -var _feat_deep_given := 0 # 深挖钻头本轮已追加份数(battle 域——v2 本格双份制:safe 事件按付费数上限) -var _feat_paid_at_round_start := 0 # 轮初付费计数快照(v2——本轮付费次数=当前值−快照) -var _feat_magma_layers := 0 # 熔火之躯积层(battle 域——受地形伤 +1,轮末反打清零) +# 效果引擎(内容数据驱动扩展批 T1——发布订阅解释器;begin_battle 从持有集/装备集重建订阅。 +# T3 起施加面全量切换:发射点 dispatch 返回命中,发射点按组合规则施加——旧 feat_flags 旗注入消亡) +var _fx: EffectEngine + + +# 事件发射薄面(16 发射点统一入口——订阅空=零循环零扰动;确定性同步分发零 RNG) +func _fx_emit(event: String, payload: Dictionary) -> Array: + if _fx == null: + return [] + return _fx.dispatch(event, payload, Callable(self, "_fx_fact")) + + +# round_start 发射+施加(内容数据驱动扩展批 T3——grant_ap mode:queue=当轮入队,行动经济轮切换 +# 即兑现;计数器 round_start 重置点在 dispatch 内生效。调用点两处:begin_battle 首轮 / 轮始段续轮) +func _emit_round_start() -> void: + for h in _fx_emit("round_start", {"round": round_index}): + if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": + action_economy.queue_ap_delta(int(h.get("value", 0))) + + +# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数) +func _fx_fact(fact: String): + match fact: + "charge_layers": + return action_economy.charge_layers + "nest_count": + var n := 0 + for eff in _status_effects.query_active("enemy"): + if String(eff.effect_id) == "weaken": + n += 1 + return n + "round_index": + return round_index + "flags_count": + var hits_n := 0 + for fevt in _facts_flags: + if bool(fevt.get("hit", false)): + hits_n += 1 + return hits_n + return null func _attune_has(item_id: String) -> bool: return attunement.has(item_id) -# 装备件表值读取(magnitude/effective_rounds 等——TreasureItemTable 行;未装配不读表) +# 装备件表值读取(charge_n 等——TreasureItemTable 行;未装配不读表。magnitude 读数面随 T4 +# 秘宝订阅迁移退役——行值经 value_from 在 rebuild 定影,零运行期按件取值) func _attune_row(item_id: String) -> Dictionary: return db.treasure_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)) - - -# 特性求值(迭代 12 T2——持有集 × feat_table → damage_controller.feat_flags 注入面): -# rider_flat = Σ rider 件 value(奥术余烬 +1 / 魔能共鸣 +2);magic 档一(≥2 件)rider +1、 -# 档二(≥4 件)rider 可暴击;clamp 两件直调键;first_dig_spell 旗(魔能共鸣第二效果——魔矢值 -# = 件 effects.value)。空集/空表 = 零旗零扰动。 -func _feat_flags_for(feat_ids: Array) -> void: - var flags: Dictionary = {} - if not feat_ids.is_empty() and db.feats != null and not db.feats.rows.is_empty(): - var hooks := FeatsEngine.hooks_for(feat_ids, db.feats) - var tiers := FeatsEngine.tiers_for(feat_ids, db.feats) - for h in hooks: - var hook: String = String(h.get("hook", "")) - var fx: Dictionary = h.get("effects", {}) as Dictionary - match hook: - "hero_attack_rider": - flags["rider_flat"] = int(flags.get("rider_flat", 0)) + int(fx.get("value", 0)) - "clamp_modifier": - # clamp 方向按件 note 词分流(魔网=magic-vs-magic 罚修正 / 湛蓝=magic-vs-heavy 加成) - if String(fx.get("polarity", "")) == "magic_vs_magic": - flags["clamp_magic_magic"] = int(flags.get("clamp_magic_magic", 0)) + int(fx.get("value", 0)) - elif String(fx.get("polarity", "")) == "magic_vs_heavy": - flags["clamp_magic_heavy"] = int(flags.get("clamp_magic_heavy", 0)) + int(fx.get("value", 0)) - elif String(fx.get("polarity", "")) == "pierce_vs_light": - flags["clamp_pierce_light"] = int(flags.get("clamp_pierce_light", 0)) + int(fx.get("value", 0)) - "first_dig_spell": - flags["first_dig_spell"] = true - flags["spell_value"] = int(fx.get("value", 0)) - "flag_resource": - flags["flag_resource_extra"] = int(fx.get("value", 0)) - "nest_aura": - flags["nest_aura_value"] = int(fx.get("value", 0)) - "flag_all_hit": - flags["flag_all_hit_mult"] = int(fx.get("value", 0)) - "flag_strike": - flags["flag_strike_value"] = int(fx.get("value", 0)) - "kill_overflow": - flags["kill_overflow"] = true - "dig_count_segments": - flags["chain_value"] = int(fx.get("value", 0)) - flags["chain_cap"] = int(fx.get("cap", 2)) - "roll_discount": - flags["discount_shift"] = int(fx.get("value", 0)) - "round_dig_bonus": - flags["round_dig_value"] = int(fx.get("value", 0)) - flags["round_dig_cap"] = int(fx.get("cap", 5)) - "hit_trade": - flags["hit_trade_atk"] = int(fx.get("trade_atk", 0)) - flags["hit_trade_hit"] = int(fx.get("trade_hit", 0)) - "armor_roll_rewrite": - flags["armor_roll_rewrite"] = int(fx.get("value", 0)) - "heavy_debuff": - flags["crack_value"] = int(fx.get("value", 0)) - "dig_yield_mult": - # 开格产出乘区族(贪婪矿灯/矿脉直觉/赌徒的血签——正负双向,同 hook 累加) - flags["dig_yield_pos_mult"] = int(flags.get("dig_yield_pos_mult", 0)) + int(fx.get("mult", 0)) - flags["dig_yield_neg_mult"] = int(flags.get("dig_yield_neg_mult", 0)) + int(fx.get("cost_mult", 0)) - if int(fx.get("cap", 0)) > 0: - flags["combo_cap"] = int(fx.get("cap", 0)) # 矿脉直觉连击上限 - "deep_dig": - flags["deep_dig_cost"] = int(fx.get("ap_cost", 0)) - flags["deep_dig_cap"] = int(fx.get("cap", 4)) - "ap_carry": - flags["ap_carry_value"] = int(fx.get("value", 0)) - "charge_shield": - flags["shield_per_layer"] = int(fx.get("shield_per_layer", 0)) - "charge_spike": - flags["spike_mult"] = int(fx.get("value", 0)) - "terrain_refund": - flags["terrain_refund"] = int(fx.get("value", 0)) - "terrain_dig_double": - flags["terrain_dig_double"] = int(fx.get("value", 0)) - "flag_neutralize": - flags["flag_neutralize"] = int(fx.get("value", 0)) - "variant_counter": - flags["variant_counter_value"] = int(fx.get("value", 0)) - var mt := int(tiers.get("magic", 0)) - if mt >= 1: - flags["rider_flat"] = int(flags.get("rider_flat", 0)) + 1 # 档一质变(≥2 件):rider +1(provisional) - if mt >= 2: - flags["rider_crit"] = true # 档二质变(≥4 件):rider 可暴击(crit 时乘暴击倍率) - _damage.feat_flags = flags - # 蓄力流/深挖钻头 action_economy 注入(求值即注入——冒烟直呼 _feat_flags_for 同面生效) - action_economy.feat_ap_carry = int(flags.get("ap_carry_value", 0)) > 0 - action_economy.feat_dig_extra = int(flags.get("deep_dig_cost", 0)) +# 特性求值(内容数据驱动扩展批 T3——_feat_flags_for 挂点求值退役:效果行为=数据行 triggers 订阅, +# 效果引擎 dispatch 分发;旧 23 hook 枚举与旗注入消亡。流派档位质变=FeatsEngine 纯函数求值 +# (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击) var _flag_miss_seen := {} # 标旗未中行去重(每安全格实例一次) var _perfect_hit_true := 0 # 板级完美排雷计数(install 重置) var _perfect_hit_false := 0 @@ -191,6 +143,7 @@ var _hero_atk := 0 # 主角攻击基值快照(encounte func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: db = db_ref rng = rng_ref + _fx = EffectEngine.new(db_ref) _board_generator = BoardGenerator.new() action_economy = ActionEconomy.new(db) minesweeper = MinesweeperRules.new(db, action_economy) @@ -199,6 +152,9 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: _reward_economy = RewardEconomy.new(db, rng) _damage = DamageController.new(db, rng, _enemy_system, _status_effects) _damage.emit_row_cb = _emit_row # 行协议归属本类(9 类行协议 + 来源断言不外泄) + _damage.set_fx(_fx) # 效果引擎注入(attack_commit/clamp_check/armor_roll/kill/damage_taken 发射面) + action_economy.set_fx(_fx) # dig_paid/charge_full 发射面 + _reward_economy.set_fx(_fx) # reward_roll 发射面 minesweeper.cell_revealed.connect(_on_cell_revealed) minesweeper.flag_judged.connect(_on_flag_judged) minesweeper.round_end_board.connect(_on_round_end) @@ -248,38 +204,44 @@ func begin_battle(enc: Dictionary) -> bool: # 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install) attunement = (enc.get("attunement", []) as Array).duplicate() _attune_charges_ref = enc.get("attune_charges", {}) # 共享 dict 引用(level/adventure 同账本——use_treasure 扣减直改) - _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 - # 特性运行态注入(迭代 12 T2 魔法流——enc.feats 携带持有集;FeatsEngine 求值后注伤害控制器 - # feat_flags;空集/空表=零扰动。魔矢每战首次开格计数同步归零) - _feat_flags_for((enc.get("feats", []) as Array)) + # 淬毒/偏运注入拆除(内容数据驱动扩展批 T4——两件被动改 triggers 订阅行:淬毒=clamp_check + # modify 窗口、偏运=reward_roll bias_rewards;value 行字段 value_from 定影,零 begin_battle 注入) + # 特性运行态(内容数据驱动扩展批 T3——enc.feats 携带持有集:效果行为=数据行 triggers 经效果 + # 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零) + var feat_ids: Array = enc.get("feats", []) as Array + if not feat_ids.is_empty() and db.feats != null: + var tiers: Dictionary = FeatsEngine.tiers_for(feat_ids, db.feats) + var mt := int(tiers.get("magic", 0)) + _damage.rider_tier_bonus = 1 if mt >= 1 else 0 + _damage.rider_tier_crit = mt >= 2 + else: + _damage.rider_tier_bonus = 0 + _damage.rider_tier_crit = false _feat_first_dig_used = false - _feat_combo = 0 - _feat_deep_given = 0 - _feat_paid_at_round_start = 0 - _feat_magma_layers = 0 - # 树 rule 键消费(迭代 12 T10——Fork 激活侧 effects;真实接线首期两键:先手压制=战斗首轮 - # AP+1(queue_ap_delta——先手压制语义)/稳健拓荒=敌方攻击减 1(伤害控制器 flags bulwark); - # 其余 rule 键=stat 面投影不入战斗(T8 表内容 provisional 口径披露)) + # 树 rule 键消费(迭代 12 T10 → 内容数据驱动扩展批 T3 换代:Fork 激活侧效果=效果引擎合成 + # 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1),旧直调拆除。其余 rule 键 + # =stat 面投影不入战斗(T8 表内容 provisional 口径披露)) var tree_rules: Array = enc.get("tree_rules", []) as Array - if tree_rules.has("posture_first_strike"): - action_economy.queue_ap_delta(1) - _damage.feat_flags["bulwark"] = 1 if tree_rules.has("posture_bulwark") else 0 + # 效果引擎订阅重建(内容数据驱动扩展批 T1——每战从持有集/装备集重建,中途获得下一战生效; + # 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放) + _fx.rebuild(feat_ids, attunement, tree_rules) # 迭代 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")) + # 工兵晨令直调拆除(内容数据驱动扩展批 T4——round_start grant_ap(mode:queue) 订阅行承载, + # when round_index ≤ effective_rounds 行字段 value_from 定影;发射位先于轮切换=当轮兑现) + # round_start 发射+施加(内容数据驱动扩展批 T3——发射位先于行动经济轮切换:树规则 grant_ap + # mode:queue 当轮入队、notify_round_start 即兑现(先手压制=首轮 AP+1 语义锚,与迁移前 + # begin_battle 直调 queue_ap_delta 逐位一致);计数器 round_start 重置点同点生效) + round_index = 1 + _emit_round_start() if not action_economy.notify_round_start(): _fatal("行动经济 首轮 notify_round_start 被拒") return false - round_index = 1 _roll_round_actions() # 开轮摇号(敌人技能池批——首轮同样摇;先于 round_started,挖掘期动画位可读) round_started.emit(round_index) return true @@ -471,42 +433,71 @@ func _settle() -> void: # 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。 _emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "") continue - _grant_routed(_feat_yield_adjust(snap, true), "positive", "safe_cell", evt.cell, ri, "") - # 开格流连击/深层产出(迭代 12 T5):矿脉直觉连击 = safe +1(mine 清零在负向分支); - # 深挖钻头深层产出 = 每 cap 格额外 grant 一份(收益面补偿额外 AP 成本——R4 甲非信息税对价) - _feat_combo += 1 - # 深挖钻头 v2(2026-10-04 二次修正——用户实测「付 2AP 无可见提升纯挨打」:v1 配额制 - # 4 次付费 8AP 换 1 份随机资源=净负期望+收益埋明细带不可感知):改**本格双份**—— - # 付费挖掘的本格资源直接双份(多花 1AP 买多一份同格产出,账目直白期望微正)。 - # 本轮付费次数 N=deep_paid_count−轮初快照;safe 事件按 N 上限逐格追加第二份(「深挖」)。 - var dd_cost := int(_damage.feat_flags.get("deep_dig_cost", 0)) - if dd_cost > 0: - var paid_this_round := int(action_economy.deep_paid_count) - _feat_paid_at_round_start - if paid_this_round > 0 and _feat_deep_given < paid_this_round: - _feat_deep_given += 1 + # dig_reveal 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: + # 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢) + var dr_hits := _fx_emit("dig_reveal", {"cell": evt.cell, "round": ri, + "is_first_dig_of_battle": not _feat_first_dig_used, + "terrain_kind": _harmful_variants(evt.get("variant_ids", []) as Array)}) + var pos_m := 0 + var neg_m := 0 + var combo_cap := -1 + for h in dr_hits: + if String(h.get("kind", "")) != "yield_mult": + continue + pos_m += int(h.get("mult", 0)) + neg_m += int(h.get("cost_mult", 0)) + if int(h.get("cap", 0)) > 0: + combo_cap = int(h.cap) # 矿脉直觉连击上限 + _grant_routed(_feat_yield_adjust(snap, true, pos_m, neg_m, combo_cap), "positive", "safe_cell", evt.cell, ri, "") + # 计数器执行(连击计数——加法族,乘区收集后执行=本格兑付用旧值,与迁移前逐位一致) + for h in dr_hits: + if String(h.get("kind", "")) == "counter_add": + var set_v = h.get("set", null) + _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) + # 追加产出(深挖钻头 v2——本格双份制:grant_material 命中=配额内追加第二份;标签随行) + for h in dr_hits: + if String(h.get("kind", "")) == "grant_material": var dd_snap = _reward_economy.grant(evt.cell) if dd_snap is Dictionary: - _grant_routed(dd_snap, "positive", "safe_cell", evt.cell, ri, "", "(深挖)") - # 魔能共鸣·每战首次开安全格追加魔法矢(迭代 12 T2——relic 件「每关首次开格必出魔法矢」 - # 落地为每战首次〔战斗域单事务无跨战状态——「每关」字面改「每战」台账披露待追认〕; - # board_typed magic 必中,扫雷结算触发点=relic 挂 first_dig_spell 承载红线①) - if bool(_damage.feat_flags.get("first_dig_spell", false)) and not _feat_first_dig_used: + _grant_routed(dd_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", ""))) + # 奥术矢(魔能共鸣——每战首次开安全格追加魔法矢;任意真族单次生效) + var spell_hits: Array = _hits_of(dr_hits, "spawn_spell") + if not spell_hits.is_empty() and not _feat_first_dig_used: _feat_first_dig_used = true var spell_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", - "奥术矢", "board", int(_damage.feat_flags.get("spell_value", 0)), + "奥术矢", "board", int((spell_hits[0] as Dictionary).get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic") _submit(spell_t, false) - # 掘进蓄能计数(迭代 11 同调批 T6——battle 域安全开格计数,每 charge_n 格积 1 点) - if _attune_has("dig_charge"): - _attune_safe_seen += 1 - var cn_b: int = int(db.treasure_item_row("dig_charge").get("charge_n", 999)) - if cn_b > 0 and _attune_safe_seen % cn_b == 0: - _attune_bank += 1 + # 掘进蓄能计数(迭代 11 同调批 T6 → 内容数据驱动扩展批 T4:积点账本迁效果引擎计数器 + # dig_charge_bank——行 triggers counter_spend(treasure_used) 受理闸同账本。模判定留编排器侧: + # 「每 N 格积 1 点」在发射点施加架构下无法行内表达先增后判(when 于 dispatch 预评估), + # 逐位等价优先——词汇表边界注记随 commit 留痕;charge_n 读行零字面量) + if _attune_has("dig_charge"): + _attune_safe_seen += 1 + var cn_b: int = int(db.treasure_item_row("dig_charge").get("charge_n", 999)) + if cn_b > 0 and _attune_safe_seen % cn_b == 0: + _fx.counter_add("dig_charge_bank", 1) elif evt.kind == "mine": # 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴), # 标中不发(旗已随挖掘擦除、不入轮末判定基线)。 - _grant_routed(_feat_yield_adjust(snap, false), "negative", "mine_hit", evt.cell, ri, "") - _feat_combo = 0 # 矿脉直觉连击清零(踩雷断连——「推理密度直接奖励」的对侧) + # mine_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: + # 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器) + var mh_hits := _fx_emit("mine_hit", {"cell": evt.cell, "round": ri, + "damage": int((snap as Dictionary).get("power", 0))}) + var halve_pct := 0 + for h in mh_hits: + if String(h.get("kind", "")) == "damage_mult": + halve_pct += int(h.get("value", 0)) + for h in mh_hits: + if String(h.get("kind", "")) == "counter_add": + var set_v = h.get("set", null) + _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) + _grant_routed(_feat_yield_adjust(snap, false, 0, 0, -1), "negative", "mine_hit", evt.cell, ri, "", "", halve_pct) + for h in mh_hits: + if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect": + var nct := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", "board", + int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {}) + _submit(nct, false) # 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径;伤害单据案C 暂存到 # 战斗段末兑付〔2026-10-06 用户裁〕;信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽; # 雷格恒无变异——布置段排除,双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径, @@ -521,55 +512,73 @@ func _settle() -> void: var vt := _damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \ int(vrow.get("effect_value", 0)), DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)}) _submit(vt, false) - # 吞噬流四面(迭代 12 T6——地形从纯税到第三资源带;伤害仍发生=乙稿判据 3 承载): - # ①荆棘编织者=挖开伤害型地形格额外掉落一次格面资源(主动吃地形换资源) - if int(_damage.feat_flags.get("terrain_dig_double", 0)) > 0: - var tw_snap = _reward_economy.grant(evt.cell) - if tw_snap is Dictionary: - _grant_routed(tw_snap, "positive", "safe_cell", evt.cell, ri, "", "(荆棘编织)") - # ②地脉采撷=受地形伤掉落小额资源补偿(薄矿化——每次受伤兑一份) - if int(_damage.feat_flags.get("terrain_refund", 0)) > 0: - var lh_snap = _reward_economy.grant(evt.cell) - if lh_snap is Dictionary: - _grant_routed(lh_snap, "positive", "safe_cell", evt.cell, ri, "", "(地脉补偿)") - # ③熔火之躯=受地形伤积层(触发键=「受地形伤」通用不按种类特化——定稿红线承载) - if int(_damage.feat_flags.get("variant_counter_value", 0)) > 0: - _feat_magma_layers += 1 + # damage_taken(kind=terrain) 发射+施加(内容数据驱动扩展批 T3——荆棘/地脉 grant_material + # 即时执行 + 熔火积层 counter_add;行序随订阅声明序〔ley 表序在 thorn 前〕,兑付值逐位一致) + var dt_hits := _fx_emit("damage_taken", {"kind": "terrain", "amount": int(vrow.get("effect_value", 0)), + "source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid)}) + for h in dt_hits: + match String(h.get("kind", "")): + "grant_material": + var tw_snap = _reward_economy.grant(evt.cell) + if tw_snap is Dictionary: + _grant_routed(tw_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", ""))) + "counter_add": + var set_v = h.get("set", null) + _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) for evt in _facts_flags: _judge_flag(evt, ri) # 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌 # 结算 board_typed 必中伤害〔复用零新配方;「旗所在巢穴敌」简化为 default_target 最前存活敌—— # 台账披露待追认〕;完美布防=本轮旗批全中(hit>0 且 miss==0)时旗伤 ×mult——全对推理奖励)。 - # 扫雷结算触发点=flag_strike/flag_all_hit/nest_aura 挂点承载红线①;伤害单据案C 暂存战斗段末兑付。 - if int(_damage.feat_flags.get("flag_strike_value", 0)) > 0: - var hits := 0 - var misses := 0 + # 伤害单据案C 暂存战斗段末兑付。 + # flag_settle 发射+施加(内容数据驱动扩展批 T3——战旗 open_ticket + 完美布防 modify_ticket + # 改写窗口:旗伤基值×完美倍率后逐旗开单,与迁移前算序一致) + var fs_hits := 0 + var fs_misses := 0 + for fevt in _facts_flags: + if bool(fevt.get("hit", false)): + fs_hits += 1 + else: + fs_misses += 1 + var fs_fx := _fx_emit("flag_settle", {"flags_hit_all": fs_hits > 0 and fs_misses == 0, + "flags_count": fs_hits, "round": ri}) + var banner_hits: Array = [] + for h in fs_fx: + if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "attack": + banner_hits.append(h) + if not banner_hits.is_empty(): + var strike_value := int((banner_hits[0] as Dictionary).get("value", 0)) + var flag_mult := 1 + for h in fs_fx: + if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "flag_mult": + flag_mult *= maxi(1, int(h.get("value", 1))) + if fs_misses == 0 and fs_hits > 0 and flag_mult > 1: + strike_value *= flag_mult # 完美布防:同轮全中旗伤倍增(全对推理奖励) for fevt in _facts_flags: - if bool(fevt.get("hit", false)): - hits += 1 - else: - misses += 1 - if hits > 0: - var mult := int(_damage.feat_flags.get("flag_all_hit_mult", 1)) - var strike_value := int(_damage.feat_flags.get("flag_strike_value", 0)) - if misses == 0 and mult > 1: - strike_value *= mult # 完美布防:同轮全中旗伤倍增(全对推理奖励) - for fevt in _facts_flags: - if not bool(fevt.get("hit", false)): - continue - var st_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_flag_strike", - "战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri, - fevt.cell, "normal") - _submit(st_t, false) - # 熔火之躯轮末反打(迭代 12 T6 吞噬流——受地形伤积层,对全体敌 ~1 必中反伤; - # 触发键=「受地形伤」通用不按地形种类特化——定稿红线承载;反打后清零;单据案C 暂存战斗段末) - var magma := int(_damage.feat_flags.get("variant_counter_value", 0)) - if magma > 0 and _feat_magma_layers > 0: - var mg_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_magma_body", - "熔火反噬 ×%d" % _feat_magma_layers, "board", magma * _feat_magma_layers, - DamageController.TARGET_ALL_ALIVE, ri, minesweeper.hero_pos(), "", 0, "", {}) - _submit(mg_t, false) - _feat_magma_layers = 0 + if not bool(fevt.get("hit", false)): + continue + var st_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_flag_strike", + "战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri, + fevt.cell, "normal") + _submit(st_t, false) + # round_end 发射+施加(内容数据驱动扩展批 T3——每轮结算收口:熔火之躯反打单开立〔open_ticket + # 即时执行〕+积层清零〔counter_add set〕;整备转蓄力由 finish_round_stop 承载——停手侧 AP 已在 + # request_stop 记账,转存决策=grant_ap(mode:transfer) 订阅定夺,无订阅走 RAP 转存缺省) + var re_fx := _fx_emit("round_end", {"round": ri}) + for h in re_fx: + if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast": + var mg_layers := _fx.counter(String(h.get("per", ""))) + var mg_total := int(h.get("value", 0)) * mg_layers + if mg_total > 0: + var mg_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_magma_body", + "熔火反噬 ×%d" % mg_layers, "board", mg_total, + DamageController.TARGET_ALL_ALIVE, ri, minesweeper.hero_pos(), "", 0, "", {}) + _submit(mg_t, false) + for h in re_fx: + if String(h.get("kind", "")) == "counter_add": + var set_v = h.get("set", null) + _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) + action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer")) if _facts_path == "cleared": _grant_clear_reward(ri) _damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界) @@ -598,31 +607,62 @@ func _settle() -> void: return _emit_row("verdict", "", Vector2i(-1, -1), ri, "下一轮次", 0, {}, "") _exit_distribution() - # 深挖钻头 v2 轮初快照滚动(本轮付费次数已消费——下一轮 N=当前值−新快照) - _feat_paid_at_round_start = int(action_economy.deep_paid_count) _settling = false # ---- 授予段 helpers(极性路由)---- -# 开格产出乘区(迭代 12 T5 开格流——贪婪矿灯/矿脉直觉/赌徒的血签;duplicate 隔离不回写快照): +# 开格产出乘区(内容数据驱动扩展批 T3 参数化——贪婪矿灯/矿脉直觉/赌徒的血签事件收集器注入): # positive=power ×(1+静态 mult% + 矿脉直觉连击加成 min(cap,combo)×10%);negative=power ×(1+cost%)。 -# 零旗=原快照原样返回(零扰动)。 -func _feat_yield_adjust(snap: Dictionary, positive: bool) -> Dictionary: - var pos_m := int(_damage.feat_flags.get("dig_yield_pos_mult", 0)) - var neg_m := int(_damage.feat_flags.get("dig_yield_neg_mult", 0)) - if positive and pos_m <= 0 and not _damage.feat_flags.has("combo_cap"): +# 全零收集器=原快照原样返回(零扰动)。combo 读数=效果引擎 dig_combo 计数器(safe+1/mine 清零, +# 跨轮累计——与迁移前 _feat_combo 逐位一致)。 +func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int, combo_cap: int) -> Dictionary: + var combo_val := _fx.counter("dig_combo") if _fx != null else 0 + if positive and pos_m <= 0 and combo_cap <= 0: return snap if not positive and neg_m <= 0: return snap var out: Dictionary = snap.duplicate() var power := int(out.get("power", int(_reward_economy.polarity_row(String(out.get("cell_type", "")), "positive" if positive else "negative").power))) var pct := pos_m if positive else neg_m - if positive and _damage.feat_flags.has("combo_cap"): - pct += mini(int(_damage.feat_flags.get("combo_cap", 5)), _feat_combo) * 10 + if positive and combo_cap > 0: + pct += mini(combo_cap, combo_val) * 10 out["power"] = maxi(1, int(round(float(power) * (1.0 + float(pct) / 100.0)))) return out + +# 伤害型变异过滤(terrain_kind fact 取数面——variation 表 effect=flat_damage 判据; +# 🔴 R4 甲永不对冲边界=仅限伤害型,信息型(hide_number 族)永不出现在本集) +func _harmful_variants(vids: Array) -> Array: + var out: Array = [] + for vid_v in vids: + var vid := String(vid_v) + var vrow: Dictionary = db.variation.row_by_id(vid) + if String(vrow.get("effect", "")) == "flat_damage": + out.append(vid) + return out + + +# 邻接伤害型变异收集(terrain_kind fact 取数面——flag_hit 发射点用:标中旗相邻八格的伤害型变异集; +# 🔴 R4 甲永不对冲边界=仅限伤害型,信息型〔hide_number 族〕不入集) +func _harmful_adjacent(cell: Vector2i) -> Array: + var out: Array = [] + var board: BoardData = minesweeper.current_board() + if board == null: + return out + for n in board.neighbors8(cell): + out.append_array(_harmful_variants(board.variant_ids(n))) + return out + + +# 效果命中过滤(按效果类型取命中子集——发射点分桶施加面) +func _hits_of(hits: Array, kind: String) -> Array: + var out: Array = [] + for h in hits: + if String((h as Dictionary).get("kind", "")) == kind: + out.append(h) + return out + # 「动作 × 格型」矩阵的执行面:snap = 缓冲期冻结身份快照(cell_type = 该格装板期摇定的类型身份), # polarity 由动作决定(开安全格/标中旗 = positive,开雷格/标错旗 = negative), # 兑付行 = 奖励字典(类型, 极性)行。返回 true = percent 行实际生效(flat 行恒 false——兑付结果由行协议承载)。 @@ -646,12 +686,14 @@ func use_treasure(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.treasure_item_row("dig_charge").get("charge_n", 0)) - return {"ok": false, "reason": "掘进蓄能:安全格积点未达(%d/%d)" % [_attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]} - _attune_bank -= 1 + # treasure_used 发射+施加(内容数据驱动扩展批 T4——使用主动秘宝受理面:counter_spend 订阅=受理闸 + # (掘进蓄能积点账本),消耗成功才受理;积点不足拒绝零消耗。按 item_id 特判随本批拆除) + var tu_hits := _fx_emit("treasure_used", {"item_id": String(v.item_id), "target": target}) + for h in tu_hits: + if String(h.get("kind", "")) == "counter_spend": + if not _fx.counter_spend(String(h.get("counter_id", "")), int(h.get("value", 1))): + var cn_info: int = int(db.treasure_item_row("dig_charge").get("charge_n", 0)) + return {"ok": false, "reason": "掘进蓄能:安全格积点未达(%d/%d)" % [_attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]} var kinds: Array = v.kinds if kinds.has("mine_reveal"): treasure_inventory.apply_overlay("mine_reveal", v.resolved) @@ -685,7 +727,7 @@ func set_treasure_unlocked(ids: Dictionary) -> void: _reward_economy.set_treasure_unlocked(ids) -func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool: +func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "", halve_pct: int = 0) -> bool: # 迭代 11 第①批 T7——aux 授予分支随掉落/停点渠道退役拆除(reward 词表第十型 treasure_item 保留但零投放: # 无 aux 行入池、CURIO_KINDS 词表已除名);剩余类型走极性字典路由。 var row: Dictionary = _reward_economy.polarity_row(String(snap.get("cell_type", "")), polarity) @@ -705,18 +747,14 @@ func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cel row = row.duplicate() row["power"] = int(round(int(row.power) * prem_pct / 100.0)) row["premium_pct"] = prem_pct - # 惊巢反制(迭代 11 同调批 T5——底稿 §3:挖开雷格该次惩罚减半 + 反弹 N;duplicate 隔离——减半 - # 不回写极性字典;仅 source_class=mine_hit 受理,标错旗/负向字典行走原通道) - if polarity == "negative" and source_class == "mine_hit" and _attune_has("nest_counter"): + # 惊巢反制(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:按 id 特判拆除——减半系数=mine_hit + # damage_mult 订阅命中(行 value 50=减 50%,取整 ceil 与迁移前逐位一致);反弹单=mine_hit + # open_ticket channel reflect 订阅施加(编排器 mine_hit 施加段);duplicate 隔离——减半不回写极性字典) + if halve_pct > 0 and polarity == "negative" and source_class == "mine_hit": row = row.duplicate() - var half: int = int(ceil(float(int(row.power)) / 2.0)) + var half: int = int(ceil(float(int(row.power)) * float(100 - halve_pct) / 100.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) @@ -840,15 +878,22 @@ 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"): + # flag_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:殉爆〔T4 秘宝面〕→ + # 净化改写 → 图谱额外资源 → 基线兑付) + var fh_hits := _fx_emit("flag_hit", {"cell": cell, "round": ri, + "terrain_kind": _harmful_adjacent(cell)}) + # 标中殉爆(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:订阅施加——行 triggers open_ticket + # channel blast value_from magnitude;每标中一面旗全体存活敌吃 N 群伤,无类型走 blast 通道) + for h in _hits_of(fh_hits, "open_ticket"): + if String(h.get("channel", "")) != "blast": + continue 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", {}) + int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {}) _submit(fbt, false) # 净化工兵(迭代 12 T6 吞噬流——标中旗中和相邻伤害型地形格):🔴 R4 甲永不对冲边界= # **仅限伤害型**(thorn/magma/rockfall——variation 表 effect=flat_damage 判据),信息型 # (hide_number 族)永不清除(红线①看门税不可对冲);中和=变普通格(BoardData.clear_variants)。 - if int(_damage.feat_flags.get("flag_neutralize", 0)) > 0: + if not _hits_of(fh_hits, "rewrite_board").is_empty(): var board: BoardData = minesweeper.current_board() for n in board.neighbors8(cell): var vids_n: Array = board.variant_ids(n) @@ -863,10 +908,11 @@ func _judge_flag(evt: Dictionary, ri: int) -> void: _emit_row("terrain_neutralized", "feat_purge_sapper", n, ri, "净化工兵·地形中和", 0, {}, "") # 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格; # 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由) - if int(_damage.feat_flags.get("flag_resource_extra", 0)) > 0: - var extra_snap = _reward_economy.grant(cell) - if extra_snap is Dictionary: - _grant_routed(extra_snap, "positive", "flag_hit", cell, ri, "", "(雷区图谱)") + for h in fh_hits: + if String(h.get("kind", "")) == "grant_material": + var extra_snap = _reward_economy.grant(cell) + if extra_snap is Dictionary: + _grant_routed(extra_snap, "positive", "flag_hit", cell, ri, "", String(h.get("label", ""))) var snap = _reward_economy.grant(cell) if not (snap is Dictionary): return @@ -941,6 +987,15 @@ func _roll_round_actions() -> void: _emit_row("highlight", "enemy_action", Vector2i(-1, -1), round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))], 0, {"skill_id": sid}, "skill_unlock") + # enemy_hp_threshold 发射点(内容数据驱动扩展批 T1——血量阈值沿;对所有带该字段敌人逐个发射, + # 非 Boss 专属——Boss 语义留给订阅者 when 判定。pct=本次解锁条目的 unlock_hp_pct 表值) + var pct_v := 0 + for pe in (u.get("pool", []) as Array): + if String((pe as Dictionary).get("skill_id", "")) == sid and (pe as Dictionary).has("unlock_hp_pct"): + pct_v = int((pe as Dictionary).unlock_hp_pct) + break + _fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v, + "is_boss": String(encounter.get("node_type", "")) == "boss"}) var entries: Array = _enemy_system.rollable_skills(u) if _enemy_system.basic_streak(u) >= pity_m: var specials: Array = _enemy_system.special_skills(u) @@ -969,10 +1024,10 @@ func _combat_segment(ri: int) -> void: round_safe += 1 elif evt.kind == "mine": round_mine = true - _damage.feat_flags["dig_streak"] = 0 if round_mine else round_safe - # 蓄力层注入(迭代 12 T5——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备减伤—— - # 三件同读 action_economy.charge_layers;battle 域每轮刷新) - _damage.feat_flags["charge_layers"] = action_economy.charge_layers + _damage.dig_streak = 0 if round_mine else round_safe + # 蓄力层注入(内容数据驱动扩展批 T3——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备 + # 减伤——三件 fact charge_layers 同读 action_economy;battle 域每轮刷新) + _damage.charge_layers = action_economy.charge_layers # ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动。案C 后授予段不再产出伤害, # 主角进入战斗段时恒存活——先手必兑,除非场上无敌)。 # 单分量伤害构成(2026-09-19 充能退役批·用户拍板):主角攻击 = 唯一基础分量(atk,吃掷点含暴击; @@ -987,14 +1042,14 @@ func _combat_segment(ri: int) -> void: var ht := _damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击", "combat", _hero_atk, \ DamageController.TARGET_FRONT_ALIVE, ri, Vector2i(-1, -1), String(_enemy_system.hero_unit().get("attack_type", "normal"))) _damage.submit(ht) - # 连珠矢(迭代 12 T4 穿刺流——开格节奏变攻击段数):本轮每开 ~3 安全格 +1 段穿刺附加 - # (上限 cap;段值 provisional 固定值不乘 atk——台账披露)。扫描雷结算触发点=dig_count_segments。 - var chain_value := int(_damage.feat_flags.get("chain_value", 0)) - if chain_value > 0: - var segments := mini(int(_damage.feat_flags.get("chain_cap", 2)), int(_damage.feat_flags.get("dig_streak", 0)) / 3) + # 连珠矢(内容数据驱动扩展批 T3——attack_commit 订阅施加面:开格节奏变攻击段数,本轮每开 + # ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位 + # 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致) + for h in _hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"): + var segments := mini(int(h.get("cap", 2)), _damage.dig_streak / maxi(1, int(h.get("divisor", 3)))) for s in segments: var ct := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_chain_arrows", - "连珠矢", "board", chain_value, DamageController.TARGET_FRONT_ALIVE, ri, + "连珠矢", "board", int(h.get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri, minesweeper.hero_pos(), "pierce") _damage.submit(ct) # 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。 @@ -1123,14 +1178,14 @@ 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.treasure_item_row("dawn_order").get("effective_rounds", 0)): - action_economy.queue_ap_delta(_attune_mag("dawn_order")) + # 工兵晨令续队直调拆除(内容数据驱动扩展批 T4——round_start 订阅行承载,见 begin_battle 注) action_economy.notify_settlement_done(mode) + # round_start 发射先于行动经济轮切换(queue 型 grant_ap 当轮入队当轮兑现——与首轮同口径) + round_index += 1 + _emit_round_start() action_economy.notify_round_start() _reset_round_facts() _refresh_used_this_round = 0 - round_index += 1 _roll_round_actions() # 开轮摇号——每轮恰摇一次(轮内刷新换图不重摇:refresh 不走本通道) round_started.emit(round_index) diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index 93fc496b..abd5002a 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -60,10 +60,16 @@ var _outcomes := {} # ticket_id → "redeemed"/"zero"/"voided"( var _seq := 0 # 流水号 var _halted := false # 主角倒下终止旗(死亡事件置位——flush 余单作废) var _hero_ac_override := -1 # 主角护甲等级合流入参(begin_battle 显式设定——-1 = 表值兜底,口径同战斗编排器) -# 特性求值旗(迭代 12 T2——begin_battle 编排器经 FeatsEngine 求值后注入;空字典=零扰动。 -# rider_flat=英雄侧必中附加值〔奥术余烬族〕/rider_crit=档二 rider 可暴击/first_dig_spell=每战首次 -# 开安全格追加魔法矢〔消费在编排器授予段〕/clamp_magic_light·clamp_magic_heavy=克制点直调) -var feat_flags: Dictionary = {} +# 效果引擎(内容数据驱动扩展批 T1/T3——attack_commit/clamp_check/armor_roll/kill/damage_taken +# 发射+施加面:dispatch 返回命中,本类按组合规则施加——T3 起旧 feat_flags 旗消费点全部拆除) +var _fx: EffectEngine = null +# 战斗段运行态事实(编排器每轮刷新——fact 取数面:attack_commit 载荷 dig_streak / +# charge_layers facts〔猎巢人 per/整备 attack_bonus per/厚积薄发 when/铁壁 shield per〕; +# 流派档位两运行态〔magic 档一 rider+1 / 档二 rider 可暴击〕) +var dig_streak := 0 +var charge_layers := 0 +var rider_tier_bonus := 0 +var rider_tier_crit := false func _init(db_ref, rng_ref, units_ref, effects_ref) -> void: @@ -79,14 +85,78 @@ func set_hero_ac_override(v: int) -> void: _hero_ac_override = v +# 效果引擎注入(战斗编排器 _init——事件发射面接线) +func set_fx(fx: EffectEngine) -> void: + _fx = fx + + +# 事件发射薄面(发射+命中收集统一入口——订阅空=零循环零扰动;命中由各施加面按组合规则施加) +func _fx_hits(event: String, payload: Dictionary) -> Array: + if _fx == null: + return [] + return _fx.dispatch(event, payload, Callable(self, "_fx_fact")) + + +# 事件发射薄面(免施加观测位——enemy_hp_threshold 等现无订阅者的事件保留零扰动面) +func _fx_dispatch(event: String, payload: Dictionary) -> void: + _fx_hits(event, payload) + + +# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数:charge_layers 编排器每轮注入) +func _fx_fact(fact: String): + match fact: + "charge_layers": + return charge_layers + "dig_streak": + return dig_streak + return null + + +# per 取数(per 字段解析——载荷同名字段优先,回落补充事实面;add_ticket_rider/attack_bonus/ +# shield/open_ticket 的倍数承载:贡献值 = value × per 读数,cap 对乘积封顶) +func _per_value(name: String, payload: Dictionary) -> int: + if name == "": + return 1 + if payload.has(name): + return int(payload[name]) + if _fx_fact(name) != null: + return int(_fx_fact(name)) + return 0 + + +# 巢穴控制数(fact nest_count 取数面——敌方 weaken 实例数,猎巢人订阅面) +func _nest_count() -> int: + var n := 0 + for eff in effects.query_active("enemy"): + if String(eff.effect_id) == "weaken": + n += 1 + return n + + # ---- 开单面(流程调用——编排器六调用点的唯一形态)---- +# 开单:A 模式当场结账;B 模式入队待 flush。返回流水号(outcome() 查询键)。 +func submit(t: Dictionary) -> int: + _seq += 1 + t["ticket_id"] = _seq + _outcomes[_seq] = "pending" + if policy == POLICY_IMMEDIATE: + _resolve(t) + else: + _queue.append(t) + return _seq + + # 单据构造(缺省填齐——调用点只写差异栏;context = kind 专用:enemy_skill 带 skill_id/baseline, -# board_flat_hero 的键并入行载荷,board_blast/board_typed 带 batch_key 供未生效行归因) +# board_flat_hero 的键并入行载荷,board_blast/board_typed 带 batch_key 供未生效行归因)。 +# attack_commit 发射点(内容数据驱动扩展批 T3——我方伤害单据开立入口,任务书 §3.1 定稿位; +# 命中挂单据 context 随行——施加面在 _resolve_hero_attack 管线算术前逐族施加,开单时点=主角攻击 +# 单恒为段内首单(结账现场与开单现场一致)。载荷 atk/target/matchup + 挖掘 streak 事实〔连珠矢/ +# 执拗订阅面〕+巢穴控制数〔猎巢人〕) func open_ticket(kind: String, source_class: String, display: String, channel: String, base_value: int, target_rule: String, ri: int, origin_cell: Vector2i = Vector2i(-1, -1), attack_type: String = "", rider: int = 0, highlight: String = "", context: Dictionary = {}) -> Dictionary: - return { + var t := { "kind": kind, "source_class": source_class, "origin_cell": origin_cell, @@ -102,18 +172,16 @@ func open_ticket(kind: String, source_class: String, display: String, channel: S "requires": -1, # 前置单据 id(-1 = 无依赖) "source_unit": {}, # enemy_skill 专用——来源敌句柄(作废判定 + commit) } - - -# 开单:A 模式当场结账;B 模式入队待 flush。返回流水号(outcome() 查询键)。 -func submit(t: Dictionary) -> int: - _seq += 1 - t["ticket_id"] = _seq - _outcomes[_seq] = "pending" - if policy == POLICY_IMMEDIATE: - _resolve(t) - else: - _queue.append(t) - return _seq + if kind == KIND_HERO_ATTACK: + var tgt = _target_for(target_rule) + var armor_b: String = String(tgt.get("armor_type", "light")) if tgt != null else "light" + var matchup_o: String = (attack_type + "_vs_" + armor_b) if attack_type != "" else "" + var ac_payload := {"atk": int(base_value), + "target": String(tgt.get("display_name", "")) if tgt != null else "", + "matchup": matchup_o, "dig_streak": dig_streak, "nest_count": _nest_count()} + t.context["fx_attack_hits"] = _fx_hits("attack_commit", ac_payload) + t.context["fx_attack_payload"] = ac_payload + return t # 同步开单(清图批专用——完美第二份与高光份数的流程依赖需要同步读数,开单权归流程 用户裁决③): @@ -189,6 +257,8 @@ func _resolve(t: Dictionary) -> void: # 主角先手攻击:掷点 → 折扣/暴击 → 克制点数 → hero_offense×enemy_defense 池乘区 → 下限 → 交付 +# (内容数据驱动扩展批 T3——attack_commit/clamp_check/armor_roll 订阅施加面:命中=开单入口随单 +# context 携带,本函数按迁移前算序逐族施加——命中序=数据表声明序,确定性) func _resolve_hero_attack(t: Dictionary) -> void: var target = _target_for(String(t.target_rule)) if target == null: @@ -196,47 +266,70 @@ func _resolve_hero_attack(t: Dictionary) -> void: return var armor: String = String(target.get("armor_type", "light")) var atk_type: String = String(t.attack_type) + var matchup := (atk_type + "_vs_" + armor) if atk_type != "" else "" + var ac_hits: Array = (t.context.get("fx_attack_hits", []) as Array) + var ac_payload: Dictionary = (t.context.get("fx_attack_payload", {}) as Dictionary) var roll_read: Dictionary = _armor_roll() var threshold: int = defender_ac(target) + int(db.settlement.attack_bonus) - # 矿工重锤·命中交换(迭代 12 T4 普攻流——正负双修原生形态:命中判定减成 trade_hit 点 - # 〔更易命中〕换攻击增伤 trade_atk%——高 AC 负期望/低 AC 正期望) - var trade_hit := int(feat_flags.get("hit_trade_hit", 0)) + # armor_roll 发射点(内容数据驱动扩展批 T1——敌护甲掷骰判定;免掷恒折扣订阅面) + var ar_hits := _fx_hits("armor_roll", {"target_ac": threshold, "roll": int(roll_read.roll)}) + # 按序改写族第一拍:矿工重锤命中交换(正负双修原生形态:命中判定减成 trade_hit 点〔更易命中〕 + # 换攻击增伤 trade_atk%——高 AC 负期望/低 AC 正期望;改写位先于掷点折扣读数,与迁移前一致) + var trade_hit := 0 + for h in ac_hits: + if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_hit", 0)) > 0: + trade_hit += int(h.trade_hit) if trade_hit > 0: threshold = maxi(1, threshold - trade_hit) var crit: bool = bool(roll_read.crit) var discount: int = _roll_discount(int(roll_read.roll), threshold) - # 专注(迭代 12 T4 普攻流——免掷骰恒定折扣:方差买断成期望——稳定器人格;roll 记 0 归因) - var rewrite := int(feat_flags.get("armor_roll_rewrite", 0)) - if rewrite > 0: - discount = rewrite - crit = false - roll_read["roll"] = 0 - # 厚积薄发满层必中(迭代 12 T5 蓄力流——spike ready 时 discount 恒全额、暴击面走倍乘不加掷) - if int(feat_flags.get("spike_mult", 0)) > 0 and int(feat_flags.get("charge_layers", 0)) >= 3: - discount = 100 - # 猎弱本能(迭代 12 T4 穿刺流——对轻甲敌折扣档改善 value 档:对冲 pierce 列 L1 −1 开局罚; + # 按序改写族第二拍:专注免掷骰恒定折扣(方差买断成期望——稳定器人格;roll 记 0 归因) + for h in ar_hits: + if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "armor_rewrite": + discount = int(h.get("value", 0)) + crit = false + roll_read["roll"] = 0 + # 按序改写族第三拍:厚积薄发满层必中(spike ready 时 discount 恒全额、暴击面走倍乘不加掷; + # when charge_layers≥3 在 dispatch 判定——满层持续状态口径) + var spike_ready := false + for h in ac_hits: + if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike": + spike_ready = true + discount = 100 + # 按序改写族第四拍:猎弱本能对轻甲敌折扣档改善 value 档(对冲 pierce 列 L1 −1 开局罚; # 改善 = 折扣表向全额方向移档,nat 最大面/已全额不重复) - var shift := int(feat_flags.get("discount_shift", 0)) + var shift := 0 + var clamp_hits := _fx_hits("clamp_check", {"matchup": matchup}) + for h in clamp_hits: + if String(h.get("kind", "")) != "modify_ticket": + continue + if String(h.get("mode", "")) == "discount_shift": + shift += int(h.get("value", 0)) if shift > 0 and armor == "light" and discount < 100: discount = _discount_shift_up(discount, shift) - var cdelta: int = _counter_delta(atk_type, armor) + _feat_clamp_bonus(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 + # 克制点数读值+订阅改写(魔网织者/湛蓝誓约/猎手刺剑 clamp_check modify delta——克制矩阵查表值 + # 后的直调;hero_attack 与 board_typed 两消费面同享) + var cdelta: int = _counter_delta(atk_type, armor) + for h in clamp_hits: + if String(h.get("kind", "")) == "modify_ticket" and h.has("delta"): + cdelta += int(h.delta) + # 淬毒刃纹(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:特判拆除——行 triggers clamp_check + # modify(mode:clamp_bonus) 订阅命中已并入上方 delta 合计;mode 路由=主角攻击面专享, + # 棋盘克制直伤窗只消费无模式 delta——棋盘不吃毒面与迁移前一致) var base_value := float(t.base_value) - var trade_atk := int(feat_flags.get("hit_trade_atk", 0)) - if trade_atk > 0: - base_value *= 1.0 + float(trade_atk) / 100.0 # 重锤增伤面(交换的正向侧) - # 蓄力流(迭代 12 T5):整备军备「每层下轮首段 +1」(层数×value 加算)+厚积薄发满层(≥3) - # spike 倍乘+强制必中(discount 100/crit 关闭掷骰面——方差买断的爆发出口) - var charge_layers := int(feat_flags.get("charge_layers", 0)) - if charge_layers > 0: - base_value += float(charge_layers * int(feat_flags.get("ap_carry_value", 0))) - var spike_mult := int(feat_flags.get("spike_mult", 0)) - var spike_ready := spike_mult > 0 and charge_layers >= 3 + # 按序改写族第五拍:矿工重锤增伤面(交换的正向侧) + for h in ac_hits: + if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_atk", 0)) > 0: + base_value *= 1.0 + float(int(h.trade_atk)) / 100.0 + # 加法族:整备军备「每层下轮首段 +1」(层数×value 加算)——attack_bonus per charge_layers + for h in ac_hits: + if String(h.get("kind", "")) == "attack_bonus": + base_value += float(int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload)) + # 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载) if spike_ready: - base_value *= float(spike_mult) + for h in ac_hits: + if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike": + base_value *= float(int(h.get("value", 1))) var rolled: int = DamageMath.rolled_component(base_value, discount, crit, int(db.settlement.crit_multiplier), cdelta) var payload: Dictionary = {"target": String(target.display_name)} payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型) @@ -247,53 +340,49 @@ func _resolve_hero_attack(t: Dictionary) -> void: payload["crit"] = true var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense"), clamped_pool("enemy_defense")) var landed: int = maxi(int(db.settlement.damage_floor), pooled) - # 英雄侧 rider(迭代 12 T2 魔法流——enemy_skill rider 对称位补齐):后置必中附加,独立于掷点 - # 与乘区(奥术余烬「普攻命中后恒定结算」语义);档二(magic 4 件)rider 可暴击=当次 crit 时 - # rider 值乘暴击倍率。rider 不改变下限语义(landed 含 rider 后不再过 floor)。 - var hero_rider := int(feat_flags.get("rider_flat", 0)) - # 猎巢人(迭代 12 T3 标旗流——巢穴控制数转伤害光环):控制数 = 敌方 weaken(削弱)实例数 - # 〔八型批 side-wide 状态行——标中旗控制巢穴的既有承载;原设计「对其中敌伤害」简化为全体 - # 先手攻击附加值——台账披露待追认〕,每控制 1 巢 rider +Z。 - var nest_aura := int(feat_flags.get("nest_aura_value", 0)) - if nest_aura > 0: - for eff in effects.query_active("enemy"): - if String(eff.effect_id) == "weaken": - hero_rider += nest_aura - # 矿工的执拗(迭代 12 T4 普攻流——「排雷排得多好,打得就有多疼」):本轮安全开格 streak - # 〔编排器授予段实时写入 feat_flags.dig_streak;踩雷清零〕×value,上限 cap。 - var rdig := int(feat_flags.get("round_dig_value", 0)) - if rdig > 0: - hero_rider += mini(int(feat_flags.get("round_dig_cap", 5)), int(feat_flags.get("dig_streak", 0)) * rdig) + # 加法族:英雄侧 rider(enemy_skill rider 对称位补齐口径)——后置必中附加,独立于掷点与乘区 + # (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成 + # (per dig_streak、cap 封顶)同通道合计;档一质变 rider+1 / 档二(crit 时)rider 乘暴击倍率。 + # rider 不改变下限语义(landed 含 rider 后不再过 floor)。 + var hero_rider := rider_tier_bonus + for h in ac_hits: + if String(h.get("kind", "")) == "add_ticket_rider": + var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload) + if h.has("cap"): + contrib = mini(int(h.cap), contrib) + hero_rider += contrib if hero_rider > 0: - if crit and bool(feat_flags.get("rider_crit", false)): + if crit and rider_tier_crit: hero_rider = int(round(float(hero_rider) * float(db.settlement.crit_multiplier))) landed += hero_rider payload["hero_rider"] = hero_rider units.apply_damage(target, landed) payload["killed"] = not target.alive # 击杀标记在扣血后取(原语义不变) - # 碎甲震击(迭代 12 T4 普攻流——命中重甲附加「护甲裂解」):复用 off_guard(enemy percent - # +25 受伤加深——零新状态行);apply_effect 域闭/重复授予由状态容器自行守卫。 - var crack := int(feat_flags.get("crack_value", 0)) - if crack > 0 and armor == "heavy": - effects.apply_effect("off_guard", {"source_class": "feat_armor_crack"}, -1) - payload["cracked"] = true + # kill 发射+施加(内容数据驱动扩展批 T3——敌人死亡确认;贯穿射击溢出弹射订阅面:对下一存活敌 + # 以穿刺类型弹射本体 value%〔无次敌=弹射落空〕。B 模式入队段尾 flush 兑现;A 模式递归 resolve) + if bool(payload.get("killed", false)): + for h in _fx_hits("kill", {"target": String(target.display_name), "overflow": landed}): + if String(h.get("kind", "")) != "overflow": + continue + var next_target = units.default_target() + if next_target != null: + var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "feat_pierce_shot", + "贯穿射击", "board", maxi(int(db.settlement.damage_floor), int(landed * int(h.get("value", 50)) / 100)), + DamageController.TARGET_FRONT_ALIVE, int(t.round_index), t.origin_cell, "pierce") + if policy == POLICY_SEGMENT: + _queue.append(of_t) + else: + _resolve(of_t) + # 即时执行族:碎甲震击打重甲附加「护甲裂解」(复用 off_guard——零新状态行;apply_effect 域闭/ + # 重复授予由状态容器自行守卫) + for h in ac_hits: + if String(h.get("kind", "")) == "apply_status": + effects.apply_effect(String(h.get("status_id", "")), {"source_class": "feat_armor_crack"}, -1) + payload["cracked"] = true var label := _attack_label(String(t.display), crit, discount) if hero_rider > 0: label += "+奥术%d(必中)" % hero_rider emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, landed, payload, String(t.highlight)) - # 贯穿射击(迭代 12 T4 穿刺流——击杀溢出弹射):击杀时对下一存活敌以穿刺类型弹射 - # 本体半额〔原设计「溢出伤害」追踪在单分量模型不可行——半额弹射 provisional 裁定披露待追认; - # 无次敌=弹射落空〕。B 模式入队段尾 flush 兑现;A 模式递归 resolve(目标存活守卫内建)。 - if bool(payload.get("killed", false)) and int(feat_flags.get("kill_overflow", 0)) > 0: - var next_target = units.default_target() - if next_target != null: - var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "feat_pierce_shot", - "贯穿射击", "board", maxi(int(db.settlement.damage_floor), int(landed / 2)), - DamageController.TARGET_FRONT_ALIVE, int(t.round_index), t.origin_cell, "pierce") - if policy == POLICY_SEGMENT: - _queue.append(of_t) - else: - _resolve(of_t) _outcomes[int(t.ticket_id)] = "redeemed" @@ -308,11 +397,14 @@ func _resolve_enemy_skill(t: Dictionary) -> void: var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.crit_multiplier), 0) var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_defense"), clamped_pool("enemy_output")) var total: int = maxi(int(db.settlement.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区 - # 铁壁整备(迭代 12 T5 蓄力流——每层下轮开场 S 点减伤;减后过下限保底) - var shield := int(feat_flags.get("shield_per_layer", 0)) * int(feat_flags.get("charge_layers", 0)) - # 稳健拓荒(迭代 12 T10——Fork posture B 侧 rule 键:受击减 1;两侧对称弱值=「不同强非强弱」) - if int(feat_flags.get("bulwark", 0)) > 0: - shield += 1 + # damage_taken 发射+施加(内容数据驱动扩展批 T3——kind=enemy_skill;铁壁整备减伤订阅面: + # shield 加法族合计 value×per(charge_layers),稳健拓荒树规则同面(rebuild 合成订阅); + # 减后过下限保底) + var shield := 0 + for h in _fx_hits("damage_taken", {"kind": "enemy_skill", "amount": total, + "source": String(t.context.get("skill_id", "")), "round": int(t.round_index)}): + if String(h.get("kind", "")) == "shield": + shield += int(h.get("value", 0)) * _per_value(String(h.get("per", "")), {}) if shield > 0: total = maxi(int(db.settlement.damage_floor), total - shield) units.apply_damage(hero, total) @@ -337,11 +429,20 @@ func _resolve_board_typed(t: Dictionary) -> void: _ineffective_row(t, "无存活敌人") _outcomes[int(t.ticket_id)] = "zero" return - var delta: int = _counter_delta(String(t.attack_type), String(target.get("armor_type", "light"))) + _feat_clamp_bonus(String(t.attack_type), String(target.get("armor_type", "light"))) - var dealt: int = DamageMath.counter_component(int(t.base_value), delta, int(db.settlement.damage_floor)) + var armor_b: String = String(target.get("armor_type", "light")) + var atk_b: String = String(t.attack_type) + # clamp_check 发射+施加(内容数据驱动扩展批 T3——克制判定修正点棋盘面;与 hero_attack 同表同点: + # modify delta 订阅改写合计进克制点数;discount_shift 档位面属主角攻击专享,本面不施加) + var cdelta: int = _counter_delta(atk_b, armor_b) + if atk_b != "": + for h in _fx_hits("clamp_check", {"matchup": atk_b + "_vs_" + armor_b}): + # 无模式 delta 才入棋盘窗(mode 路由:clamp_bonus=主角攻击面专享、discount_shift=折扣档面) + if String(h.get("kind", "")) == "modify_ticket" and h.has("delta") and String(h.get("mode", "")) == "": + cdelta += int(h.delta) + var dealt: int = DamageMath.counter_component(int(t.base_value), cdelta, int(db.settlement.damage_floor)) units.apply_damage(target, dealt) emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → %s" % [String(t.display), String(target.display_name)], dealt, - {"target": String(target.display_name), "attack_type": String(t.attack_type), "effectiveness": delta}, String(t.highlight)) + {"target": String(target.display_name), "attack_type": String(t.attack_type), "effectiveness": cdelta}, String(t.highlight)) _outcomes[int(t.ticket_id)] = "redeemed" @@ -418,23 +519,6 @@ func _counter_delta(attack_type: String, armor: String) -> int: return int(row.get(armor, 0)) -# 克制点直调(迭代 12 T2 魔法流/T4 穿刺流 clamp_modifier 件——克制矩阵查表值后的特性调整; -# 空旗=0 零扰动):魔网织者=magic vs magic(矩阵罚 −1 → 0/正);湛蓝誓约=magic vs heavy(+1 → +2); -# 猎手刺剑=pierce vs light(+1——对轻甲敌穿刺提升,对重甲无增益=正负双修)。hero_attack 与 -# board_typed 两消费面同享。 -func _feat_clamp_bonus(attack_type: String, armor: String) -> int: - var bonus := 0 - if attack_type == "magic": - if armor == "magic": - bonus += int(feat_flags.get("clamp_magic_magic", 0)) - elif armor == "heavy": - bonus += int(feat_flags.get("clamp_magic_heavy", 0)) - elif attack_type == "pierce": - if armor == "light": - bonus += int(feat_flags.get("clamp_pierce_light", 0)) - return bonus - - # 折扣档改善(迭代 12 T4 猎弱本能——折扣表向全额方向移 value 档;查不到当前档 = 不动防御口径) func _discount_shift_up(discount: int, shift: int) -> int: var tbl: Array = db.settlement.roll_tier_discounts @@ -444,9 +528,6 @@ func _discount_shift_up(discount: int, shift: int) -> int: return int(tbl[mini(tbl.size() - 1, idx + shift)]) -# 淬毒刃纹 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/effect_engine.gd b/game/core/effect_engine.gd new file mode 100644 index 00000000..54bc60ad --- /dev/null +++ b/game/core/effect_engine.gd @@ -0,0 +1,343 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# Date: 2026-10-06(迭代 17 卡⓪ 内容数据驱动扩展批 T1——效果引擎框架) +# 内容数据驱动扩展(任务书 design/research/任务书-内容数据驱动扩展批-2026-10-06.md;设计权威 +# design/research/研究-内容数据驱动扩展架构-2026-10-06.md):把「内容的效果行为」从散布在结算 +# 代码里的硬编码分支搬进内容数据行三个字段——订阅哪个事件(on)/ 什么条件(when)/ 什么效果(do) +# ——由本解释器统一执行。完成后加一件「已有效果类型」的新专长/新秘宝=只改 .tres 数据行,零代码。 +# 架构模型(R1 裁决):发布订阅——结算管线同步 emit 事件(无队列无异步,确定性优先)→ 本引擎遍历 +# 持有订阅集匹配 → 返回按声明序排列的效果命中(do 列表),由发射点按组合规则施加。 +# 多订阅与冲突(R3 裁决):一事件多订阅=常态;冲突在效果类型层用组合规则封死(§3.4 六族);遍历序 +# =数据表声明序(feats 表序→treasure 表序→树规则合成行,装载时定死),写死保种子复放。 +# 重建时机(v1.1 补):每战 begin_battle 从持有集/装备集重建 trigger 列表——冒险中途获得/换取的 +# 专长下一战生效,同战内不热载,确定性优先。计数器账本随重建清零(battle 域)。 +# 确定性契约:全同步调用、零 RNG、零 signal 时序依赖;同输入同命中序。 +# 护栏保留:事件/效果/事实/算子四词表封闭(本文件常量),行内出域=加载期校验致命(schema 侧), +# 运行期 dispatch 对未知事件/效果类型 fail-fast(push_error+断言)。 +class_name EffectEngine +extends RefCounted + +## 事件词汇(16 个发射点——任务书 §3.1 定稿;4 个登记暂不做事件不入本表) +const EVENTS := ["dig_reveal", "dig_paid", "flag_hit", "mine_hit", "attack_commit", "clamp_check", + "armor_roll", "kill", "damage_taken", "enemy_hp_threshold", "round_start", "flag_settle", + "round_end", "reward_roll", "treasure_used", "charge_full"] + +## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥1 行订阅本集事件——schema 加载校验。 +## 原 MINE_HOOKS 的同义迁移:挖掘/标旗/轮末结算面事件) +const MINE_EVENTS := ["dig_reveal", "dig_paid", "flag_hit", "mine_hit", "flag_settle", "round_end", "reward_roll"] + +## 效果类型词汇(解释器 switch 闭集——任务书 §3.3 定稿 19 行;类型实数 24,含共享行拆型: +## grant_gp/bias_wallet、counter_add/counter_spend、reveal_tiles/remove_mines/refresh_pick、 +## bias_rewards/preview。任务书标题计 23 系笔误,落码按行内容全收——commit 留痕) +const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open_ticket", + "attack_bonus", "attack_segments", "yield_mult", "spawn_spell", "overflow", "grant_material", + "grant_ap", "grant_gp", "bias_wallet", "shield", "spike", "apply_status", "counter_add", + "counter_spend", "rewrite_board", "reveal_tiles", "remove_mines", "refresh_pick", + "bias_rewards", "preview"] + +## fact 条件词汇(封闭枚举 9 项——任务书 §四定稿) +const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_armor_class", + "flags_count", "charge_layers", "nest_count", "round_index", "counter"] + +## when 算子词汇(封闭——fact 查询面落法随批登记:mod_zero=计数器取模判满〔掘进蓄能〕、 +## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较〔深挖钻头配额〕、 +## ends_with=后缀匹配〔对某甲族的泛条件〕。均系映射表现状条件装进 9 项 fact 的查询面落法, +## 代拍板白名单覆盖,commit 留痕) +const OPS := ["eq", "neq", "gt", "gte", "lt", "lte", "ends_with", "has", "mod_zero", + "advantageous", "gte_ref", "lt_ref"] + +## when 条件键白名单(trigger 行 when 数组元素键) +const WHEN_KEYS := ["fact", "op", "value", "id", "ref", "value_from"] + +## do 效果键白名单(trigger 行 do 字典键——零裸 flat 同族护栏:kind 外键全部参数引用面) +const DO_KEYS := ["kind", "value", "value_from", "delta", "delta_from", "mult", "mult_from", + "cap", "cap_from", "cost_mult", "per", "divisor", "dtype", "mode", "channel", "counter_id", + "reset_at", "set", "status_id", "label", "fallback", "target_kinds", "trade_atk", "trade_hit"] + +## 主动件分发通道(不经事件订阅——刷新/显影/移除是玩家主动动作;use_treasure 按 kind 分发) +const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"] + +# ---- 订阅集与计数器账本 ---- +var _triggers: Array = [] # [{source, id, on, when, do}](装载序=声明序,重建时定死) +var _counters: Dictionary = {} # counter_id → int(battle 域账本;重建清零;重置点由订阅行声明) +var _reset_on_round_start: Array[String] = [] # round_start 重置点计数器名单(装载时收集) +var db: ConfigDb # 矩阵/表读取(advantageous 算子消费)——可空(纯计数器用法零依赖) + + +func _init(db_ref: ConfigDb = null) -> void: + db = db_ref + + +## 订阅装载(每战 begin_battle 调用——重建时机 v1.1):feats 表序→treasure 表序(各按持有/装备 +## 集过滤)→树规则合成行。同一数据行的多个 trigger 按行内数组序。计数器账本同步清零并按订阅行 +## 声明收集重置点(round_start)。 +func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void: + _triggers.clear() + _counters.clear() + _reset_on_round_start.clear() + if db != null and db.feats != null: + for row in db.feats.rows: + var fid := String(row.get("feat_id", "")) + if not feat_ids.has(fid): + continue + for tr in (row.get("triggers", []) as Array): + _push_trigger("feat", fid, tr, row) + if db != null: + for row in db.treasure_attunement_rows(): + var iid := String(row.get("item_id", "")) + if not item_ids.has(iid): + continue + for tr in (row.get("triggers", []) as Array): + _push_trigger("attune", iid, tr, row) + # 树规则合成订阅(T5——规则键效果走同面:非数据行,词表同闭集) + for key_v in tree_rules: + var rk := String(key_v) + match rk: + "posture_first_strike": + _push_trigger("tree", rk, {"on": "round_start", + "when": [{"fact": "round_index", "op": "eq", "value": 1}], + "do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {}) + "posture_bulwark": + _push_trigger("tree", rk, {"on": "damage_taken", + "do": {"kind": "shield", "value": 1}}, {}) + + +func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary) -> void: + var on := String(tr.get("on", "")) + assert(EVENTS.has(on), "[效果引擎] 订阅事件出域: %s(%s %s)" % [on, source, id]) + var do_d: Dictionary = (tr.get("do", {}) as Dictionary).duplicate(true) + # *_from 解析(rebuild 时把行字段引用定影为值——value_from:"magnitude" → value=行 magnitude) + for pair in [["value_from", "value"], ["delta_from", "delta"], ["mult_from", "mult"], ["cap_from", "cap"]]: + if do_d.has(pair[0]): + do_d[pair[1]] = int(row.get(String(do_d[pair[0]]), 0)) + do_d.erase(pair[0]) + # when value_from 同面解析(行字段引用条件——dawn_order effective_rounds 先例:字面量出行=数值 + # 单一事实源破面,行引用随 rebuild 定影) + var when_d: Array = (tr.get("when", []) as Array).duplicate(true) + for c_v in when_d: + var c: Dictionary = c_v as Dictionary + if c.has("value_from"): + c["value"] = int(row.get(String(c["value_from"]), 0)) + c.erase("value_from") + var t := {"source": source, "id": id, "on": on, + "when": when_d, + "do": do_d} + _triggers.append(t) + # 计数器重置点收集(counter_add/counter_spend 行声明 reset_at) + if do_d.has("counter_id") and String(do_d.get("reset_at", "")) == "round_start": + var cid := String(do_d.counter_id) + if not _reset_on_round_start.has(cid): + _reset_on_round_start.append(cid) + + +## 订阅查询面(发射点缺省行为仲裁用——如 round_end 无 transfer 订阅时走 RAP 转存) +func has_effect_on(event: String, kind: String, mode: String = "") -> bool: + for tr in _triggers: + if String(tr.on) != event: + continue + var d: Dictionary = tr.do + if String(d.get("kind", "")) == kind and (mode == "" or String(d.get("mode", "")) == mode): + # 有 when 时不能预判命中(发射点缺省行为保守按「可能命中」计——命中由 dispatch 定夺) + return true + return false + + +## 事件分发(发射点唯一入口):返回命中效果列表(装载序=声明序;零命中=空数组零扰动)。 +## payload = 事件现场(fact 取值第一优先源);facts = 补充事实面(charge_layers/nest_count 等 +## 运行态读数 Callable——payload 未带时回落)。 +func dispatch(event: String, payload: Dictionary, facts: Callable = Callable()) -> Array: + assert(EVENTS.has(event), "[效果引擎] 未知事件: %s(发射点出域)" % event) + if event == "round_start": + for cid in _reset_on_round_start: + _counters[cid] = 0 + var hits: Array = [] + for tr in _triggers: + if String(tr.on) != event: + continue + if not _when_ok(tr.when, payload, facts): + continue + hits.append(tr.do) + return hits + + +## 计数器读数(fact counter 与发射点配额面共用) +func counter(counter_id: String) -> int: + return int(_counters.get(counter_id, 0)) + + +func counter_add(counter_id: String, value: int, set_mode: bool = false) -> void: + if set_mode: + _counters[counter_id] = value + else: + _counters[counter_id] = int(_counters.get(counter_id, 0)) + value + + +## 计数器消耗(counter_spend 组合规则——按订阅序消耗共享资源,耗尽即止): +## 返回 true=消耗成功;false=资源不足(调用方按 reason 通道拒绝)。 +func counter_spend(counter_id: String, value: int) -> bool: + var cur := int(_counters.get(counter_id, 0)) + if cur < value: + return false + _counters[counter_id] = cur - value + return true + + +# ---- when 判定(条件谓词——零随机全确定) ---- + +func _when_ok(when: Array, payload: Dictionary, facts: Callable) -> bool: + for c_v in when: + if not _cond_ok(c_v as Dictionary, payload, facts): + return false + return true + + +func _cond_ok(c: Dictionary, payload: Dictionary, facts: Callable) -> bool: + var fact := String(c.get("fact", "")) + var op := String(c.get("op", "eq")) + var val = c.get("value", null) + var actual = _fact_value(fact, c, payload, facts) + match op: + "eq": + return str(actual) == str(val) + "neq": + return str(actual) != str(val) + "gt": + return int(actual) > int(val) + "gte": + return int(actual) >= int(val) + "lt": + return int(actual) < int(val) + "lte": + return int(actual) <= int(val) + "ends_with": + if val is Array: + for v in (val as Array): + if String(actual).ends_with(String(v)): + return true + return false + return String(actual).ends_with(String(val)) + "has": + # 在场判定(fact 查询面落法——代拍板白名单,commit 留痕):value 缺省=非空判定 + # (terrain_kind fact:伤害型变异集非空=挖开伤害型地形格/邻格有伤害型地形/地形伤种类在场); + # value 给定=数组包含判定(保留泛用面)。 + if c.has("value"): + if actual is Array: + return (actual as Array).has(val) + return false + if actual is Array: + return not (actual as Array).is_empty() + return String(actual) != "" + "mod_zero": + var m := int(val) + return m > 0 and int(actual) % m == 0 + "advantageous": + # 克制矩阵正面判定(matchup = "{attack_type}_vs_{armor}"——矩阵值 > 0 = 克制成立) + return _counter_delta_of(String(actual)) > 0 + "gte_ref": + return int(actual) >= counter(String(c.get("ref", ""))) + "lt_ref": + return int(actual) < counter(String(c.get("ref", ""))) + assert(false, "[效果引擎] when 算子出域: %s" % op) + return false + + +func _fact_value(fact: String, c: Dictionary, payload: Dictionary, facts: Callable): + match fact: + "counter": + return counter(String(c.get("id", ""))) + _: + # fact 取值序:payload 同名字段优先 → 补充事实面(发射点注入的运行态读数) + if payload.has(fact): + return payload[fact] + if facts.is_valid(): + return facts.call(fact) + return null + + +## 克制矩阵读值(advantageous 算子消费——与伤害控制器 _counter_delta 同源同表) +func _counter_delta_of(matchup: String) -> int: + if db == null or matchup == "" or not matchup.contains("_vs_"): + return 0 + var parts := matchup.split("_vs_") + if parts.size() != 2: + return 0 + var tbl: Dictionary = db.settlement.armor_effectiveness + var row: Dictionary = tbl.get(String(parts[0]), {}) + return int(row.get(String(parts[1]), 0)) + + +## ---- 加载期校验(schema 侧共用——单一事实源;悬空引用/词表出域=致命) ---- + +## trigger 行校验:返回该行是否含扫雷侧事件订阅(流派铁律聚合面)。 +## row 用于 value_from 类引用的字段存在性判定(悬空=加载致命)。 +static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array, errs: Array) -> bool: + var mine_covered := false + for tr_v in triggers: + var tr: Dictionary = tr_v as Dictionary + for k in tr.keys(): + if not ["on", "when", "do"].has(String(k)): + errs.append({"category": "zero_flat", "message": "%s trigger 键出白名单: %s" % [rid, String(k)]}) + var on := String(tr.get("on", "")) + if not EVENTS.has(on): + errs.append({"category": "enum", "message": "%s 订阅事件出域(事件词汇封闭): %s" % [rid, on]}) + elif MINE_EVENTS.has(on): + mine_covered = true + for c_v in (tr.get("when", []) as Array): + var c: Dictionary = c_v as Dictionary + for k in c.keys(): + if not WHEN_KEYS.has(String(k)): + errs.append({"category": "zero_flat", "message": "%s when 键出白名单: %s" % [rid, String(k)]}) + var fact := String(c.get("fact", "")) + if not FACTS.has(fact): + errs.append({"category": "enum", "message": "%s fact 出域(事实词汇封闭 9 项): %s" % [rid, fact]}) + if not OPS.has(String(c.get("op", "eq"))): + errs.append({"category": "enum", "message": "%s when 算子出域: %s" % [rid, String(c.get("op", ""))]}) + var do_d: Dictionary = tr.get("do", {}) as Dictionary + for k in do_d.keys(): + if not DO_KEYS.has(String(k)): + errs.append({"category": "zero_flat", "message": "%s do 键出白名单: %s(机制轴禁无条件属性加项)" % [rid, String(k)]}) + var kind := String(do_d.get("kind", "")) + if not EFFECT_KINDS.has(kind): + errs.append({"category": "enum", "message": "%s 效果类型出域(解释器闭集): %s" % [rid, kind]}) + # value_from/delta_from/mult_from/cap_from 引用面:行字段存在性(悬空=加载致命)—— + # do 与 when 两侧同面(when value_from=dawn_order effective_rounds 先例) + for vf_k in ["value_from", "delta_from", "mult_from", "cap_from"]: + if do_d.has(vf_k): + var fld := String(do_d.get(vf_k, "")) + if fld != "" and not row.has(fld): + errs.append({"category": "dangling", "message": "%s do.%s 悬空(行无字段 %s)" % [rid, vf_k, fld]}) + for c_v in (tr.get("when", []) as Array): + var c_w: Dictionary = c_v as Dictionary + if c_w.has("value_from"): + var wfld := String(c_w.get("value_from", "")) + if wfld != "" and not row.has(wfld): + errs.append({"category": "dangling", "message": "%s when.value_from 悬空(行无字段 %s)" % [rid, wfld]}) + return mine_covered + + +## 组合规则(任务书 §3.4 六族——效果类型的必填属性,发射点施加面引用) + +## 加法族(数值相加,可交换):add_ticket_rider、attack_bonus、grant_material、grant_ap(非 transfer)、 +## counter_add、shield——发射点逐条累加收集器。 +## 乘法族(收集后一次施加,正负分桶):yield_mult、damage_mult。 +## 任意真族(任一命中即生效):spawn_spell、overflow、spike。 +## 按序改写族(收集→单点按声明序施加):modify_ticket 及一切改写窗口效果。 +## 按序消耗族(按订阅序消耗共享资源,耗尽即止;可选 fallback):counter_spend。 +## 即时执行族(每条命中各自执行一次,执行序=声明序):open_ticket、grant_ap(mode:transfer)、 +## rewrite_board、apply_status——发射点逐条执行。 +static func combo_of(kind: String) -> String: + match kind: + "add_ticket_rider", "attack_bonus", "grant_material", "counter_add", "shield": + return "additive" + "grant_ap": + return "additive" # mode:transfer 由发射点即时执行族承载(转移非加法) + "yield_mult", "damage_mult": + return "multiplicative" + "spawn_spell", "overflow", "spike": + return "any_true" + "modify_ticket": + return "ordered_write" + "counter_spend": + return "ordered_spend" + "open_ticket", "rewrite_board", "apply_status": + return "immediate" + return "immediate" # 默认即时执行(每条命中各自执行一次) diff --git a/game/core/effect_engine.gd.uid b/game/core/effect_engine.gd.uid new file mode 100644 index 00000000..d63bf9a6 --- /dev/null +++ b/game/core/effect_engine.gd.uid @@ -0,0 +1 @@ +uid://dgo4b5frip3e5 diff --git a/game/core/feats_engine.gd b/game/core/feats_engine.gd index 3b7a5ebb..f44526e9 100644 --- a/game/core/feats_engine.gd +++ b/game/core/feats_engine.gd @@ -30,21 +30,8 @@ static func tiers_for(feat_ids: Array, table: FeatTable) -> Dictionary: return out -## 挂点投影:持有集 → [{feat_id, hook, archetype, tier, effects}](表序稳定——同集合同投影, -## MC 逐位可复放承载) -static func hooks_for(feat_ids: Array, table: FeatTable) -> Array: - var out: Array = [] - for rid in feat_ids: - var row: Dictionary = table.row_by_id(String(rid)) if table != null else {} - if row.is_empty(): - continue - out.append({ - "feat_id": String(row.get("feat_id", "")), - "hook": String(row.get("hook", "")), - "archetype": String(row.get("archetype", "")), - "effects": (row.get("effects", {}) as Dictionary).duplicate(true), - }) - return out +## 挂点投影随 T3 退役(内容数据驱动扩展批——hook 词族消亡:订阅投影=效果引擎 rebuild 装载序 +## (feats 表序),投影稳定性由 EffectEngine._triggers 声明序承载) ## 成型判读(读数面口径):流派 tier ≥1 = 成型(档一即判——任务书完成定义「标签+档位激活引擎可判成型」) diff --git a/game/core/reward_economy.gd b/game/core/reward_economy.gd index 57943374..4322dc85 100644 --- a/game/core/reward_economy.gd +++ b/game/core/reward_economy.gd @@ -23,6 +23,12 @@ var _granted := {} # Vector2i → 快照(重复受理幂等返 var _clear_identity := "" var _clear_granted := false var _clear_snapshot := {} +# 效果引擎(内容数据驱动扩展批 T1——reward_roll 发射面;偏运硬币 bias_rewards 订阅面) +var _fx: EffectEngine = null + + +func set_fx(fx: EffectEngine) -> void: + _fx = fx func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: @@ -59,7 +65,14 @@ func roll_board(eligible: Array) -> bool: _clear_granted = false _clear_snapshot = {} var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号) - var cell_rows: Array = _biased_cell_rows() # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰) + # reward_roll 发射+施加(内容数据驱动扩展批 T4——格面资源掷定、摇号前权重合成层: + # bias_rewards 订阅命中合计=偏运权重加算(行 value_from magnitude 定影);零命中=原行集零扰动) + var luck_bias := 0 + if _fx != null: + for h in _fx.dispatch("reward_roll", {"polarity": "positive", "cell_count": list.size()}): + if String(h.get("kind", "")) == "bias_rewards": + luck_bias += int(h.get("value", 0)) + var cell_rows: Array = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰) var clear_rows: Array = _pool_rows("clear_default") # ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布) var draft := {} @@ -174,8 +187,8 @@ func polarity_row(cell_type: String, polarity: String) -> Dictionary: # ---- F1 选行算子纯函数 ---- -# 偏运硬币 N(迭代 11 同调批 T5——战斗结算 begin_battle 注入;0 = 未装配零扰动) -var luck_bias: int = 0 +# 偏运硬币 N(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:begin_battle 注入拆除—— +# reward_roll bias_rewards 订阅命中合计经 _biased_cell_rows 入参;0 = 未装配零扰动) # 稀有正向资源 kinds(底稿 §3——权重投影面,池结构零触碰) const LUCK_BIAS_KINDS := ["heal", "gp", "pierce", "magic"] @@ -192,15 +205,15 @@ const SKEW_UP_KINDS := ["attack", "blast", "pierce", "magic"] # 偏运硬币权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动) # 基座换源(首发批 2026-10-04):先经图级偏斜投影再叠件级加法——合成序锚=图级先、件级后 #(环境事实先定型、玩家构筑叠其上;两投影均为池内权重面,雷安同池与取号次数零变化) -func _biased_cell_rows() -> Array: +func _biased_cell_rows(bias: int) -> Array: var base: Array = _skewed_cell_rows() - if luck_bias <= 0: + if bias <= 0: return base var out: Array = [] for row in base: if LUCK_BIAS_KINDS.has(String(row.get("cell_type", ""))): var biased: Dictionary = row.duplicate() - biased["weight"] = int(row.weight) + luck_bias + biased["weight"] = int(row.weight) + bias out.append(biased) else: out.append(row) diff --git a/game/data/schemas/adventure_table.gd b/game/data/schemas/adventure_table.gd index 3cc181b8..bd5dd0b4 100644 --- a/game/data/schemas/adventure_table.gd +++ b/game/data/schemas/adventure_table.gd @@ -80,6 +80,23 @@ const TIER_QUOTAS := {"common": 70, "rare": 25, "rarest": 3} @export var feat_ticket_pity_step: int = 5 # [1,99] knob(口径定死=5)——保底步数(连续未出展示次数) +## 令牌兑付路由表(内容数据驱动扩展批 T5——_apply_curio_pick match 收敛进效果类型: +## 属性值提升四类 = 「加算落点键」声明路由,加新加算类令牌 = 表行+配额/件值键,零代码; +## 通道面三类(gp 钱包/heal 台账/refresh 界面余量)+relic 持有集 = 副作用通道显式分支不进本表 +## ——路由表只承载「纯加算落点」形态,与效果引擎组合规则加法族同构) +const CURIO_APPLY_ROUTES := { + "attack": "atk", + "max_hp": "max_hp", + "armor": "armor_class", + "ap": "ap", +} + + +## 令牌加算路由读数(空 = 非加算形态——调用方走通道面分支) +func curio_apply_route(kind: String) -> String: + return String(CURIO_APPLY_ROUTES.get(kind, "")) + + ## 令牌配额读数(未列 kind×tier 组合 = 0——heal 无普通档/ap 无普通稀有档按 v2.2 构成) func curio_quota(kind: String, tier: String) -> int: match [kind, tier]: diff --git a/game/data/schemas/feat_table.gd b/game/data/schemas/feat_table.gd index 6f43a3d4..fcd0dab1 100644 --- a/game/data/schemas/feat_table.gd +++ b/game/data/schemas/feat_table.gd @@ -1,7 +1,8 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 七核心流派各 ≥3 件入局内随机渠道后,标签+档位激活引擎能否机械判「成型」、 # 专长带能否挂进令牌池稀有/罕见档随机投放(P0——迭代 12 双层 BD 实施批 T1) -# Date: 2026-10-04(双层 BD 实施批 T1——任务书五裁定稿;设计权威 双层BD体系-整合定稿-2026-10-03 v3) +# Date: 2026-10-06(内容数据驱动扩展批 T3——行换代:hook/effects 挂点形态退役,triggers 事件订阅形态 +# 唯一合法行形——旧 23 挂点枚举消亡:事件名+效果类型取代,任务书 R4 裁决) # 概念指路:本表的专长,与 adventure.tres 的属性值提升、资源,同属一个对象—— # 「奇物」(一次冒险内各渠道给予玩家的增益总称)。定义见 design/gdd/systems/冒险循环/奇物.md。 # 它们是它分类轴上的枚举(专长 / 属性值提升 / 资源),不是三套系统;本表承载「专长」(专长 Feat)的逐项定义。 @@ -11,16 +12,15 @@ # 数据配置 FeatTable schema(奇物·专长定义表;行 = Dictionary 键 feat_id,Rule 4 豁免 # 定义表——TreasureItemTable 先例)。 # 行字段:feat_id 主键 / display_name / rarity ∈ {common, rare, rarest}(对齐令牌池三档命名—— -# 投放档映射:common/rare→稀有令牌档、rarest→罕见令牌档;relic 令牌永不入普通档=「挂稀有/罕见档」 -# 追认 b 接线口径)/ archetype ∈ ARCHETYPES 七流派标签 / hook ∈ HOOKS 挂点枚举(结算段槽——整合定稿 -# §2.1 哈迪斯恩赐槽同构)/ effects Dictionary(hook 消费参数,provisional 投值)。 -# 正负双修(整合定稿 B2 判据):effects 允许负向键(cost_* 族)——纯增益件须专项论证(反例 B-3),加载期 -# 仅校验键域不禁纯增益(专项论证件以 positive_only 批注登记,人工清单呈报)。 +# 投放档映射:common/rare→稀有令牌档、rarest→罕见令牌档;relic 令牌永不入普通档)/ +# archetype ∈ ARCHETYPES 七流派标签 / desc 效果说明(文案表值化——呈现层读行,零硬编码)/ +# triggers 订阅行数组(订阅哪个事件 on / 什么条件 when / 什么效果 do——效果引擎 EffectEngine +# 统一解释执行;四词表封闭校验=EffectEngine.validate_trigger_row 单一事实源)。 # 档位激活(整合定稿 设计工具 1——以撒套装/小骨铭文):同标签计数 ≥2→档一(流派核心规则)/≥4→档二 # (完全体);散件单体保底=每件效果独立生效,档位只做质变(反例 B-2)——引擎纯函数 FeatsEngine。 # 🔴 层级纪律(任务书红线 2):专长只走局内随机渠道(三选一/商店/格面)——本表行永不入同调位 -# (treasure_items 表)/局外树(T8 节点表);扫雷触发点铁律:每流派 ≥1 行 hook ∈ MINE_HOOKS(加载校验, -# 违反=红线①触碰面)。 +# (treasure_items 表)/局外树(T8 节点表);扫雷触发点铁律:每流派 ≥1 行订阅扫雷侧事件 +# (EffectEngine.MINE_EVENTS——加载校验,违反=红线①触碰面)。 class_name FeatTable extends Resource @@ -32,58 +32,20 @@ const ARCHETYPES := ["magic", "pierce", "normal", "dig", "flag", "charge", "devo ## 稀有度词表(对齐令牌池三档;投放档映射 relic→rare / rarest→rarest) const RARITIES := ["common", "rare", "rarest"] -## 挂点枚举(结算段槽——首批 23 点位;每 hook 恰一流派域消费,T2-T6 接线) -const HOOKS := [ - # 魔法流 - "hero_attack_rider", # 普攻命中后追加必中魔法伤(奥术余烬/魔能共鸣) - "clamp_modifier", # 克制点直调(魔网织者/湛蓝誓约/猎手刺剑) - "first_dig_spell", # 每关首次开格必出魔法矢(魔能共鸣第二效果) - # 穿刺流 - "kill_overflow", # 击杀溢出弹射(贯穿射击) - "dig_count_segments", # 开格计数转攻击段数(连珠矢) - "roll_discount", # 折扣档改写(猎弱本能前移一档) - # 普攻流 - "round_dig_bonus", # 轮内安全开格数转攻击加成(矿工的执拗;踩雷清零=mine_hit 监听同口) - "hit_trade", # 命中交换(矿工重锤——正负双修原生形态) - "armor_roll_rewrite", # 免掷骰恒折扣(专注) - "heavy_debuff", # 命中重甲附加护甲裂解(碎甲震击) - # 开格流 - "dig_yield_mult", # 开格产出乘区(贪婪矿灯/矿脉直觉/赌徒的血签——正负双向) - "deep_dig", # 深层资源掘取(深挖钻头——R4 甲非信息税形态:开掘额外 AP 代价) - # 标旗流 - "flag_resource", # 标中雷获格面资源(雷区图谱) - "nest_aura", # 控制巢穴数转伤害光环(猎巢人) - "flag_all_hit", # 同轮全中旗伤倍增(完美布防) - "flag_strike", # 轮末旗伤开单(战旗军规——复用 board_typed) - # 蓄力流 - "ap_carry", # 轮末 AP 结转蓄力(整备军备——改写 cleared 作废口径) - "charge_shield", # 蓄力层转减伤(铁壁整备) - "charge_spike", # 满层 spike(厚积薄发) - # 吞噬流 - "terrain_refund", # 受地形伤掉落资源补偿(地脉采撷——伤害仍发生) - "terrain_dig_double", # 挖开伤害型地形格双产出(荆棘编织者) - "flag_neutralize", # 标中旗中和相邻伤害型地形格(净化工兵——🔴 R4 甲仅限伤害型) - "variant_counter", # 受地形伤积层轮末反打(熔火之躯——触发键不按地形种类特化) -] +## 行键白名单(🔴 零裸 flat 同族护栏——键出域=加载致命;triggers/desc=T3 换代新形态) +const ROW_KEYS := ["feat_id", "display_name", "rarity", "archetype", "triggers", "desc", "note"] -## 扫雷结算触发点 hook 集(🔴 红线①承载:每流派 ≥1 行 hook 命中本集——加载校验) -const MINE_HOOKS := ["first_dig_spell", "dig_count_segments", "round_dig_bonus", "dig_yield_mult", - "deep_dig", "flag_resource", "nest_aura", "flag_all_hit", "flag_strike", "ap_carry", - "terrain_refund", "terrain_dig_double", "flag_neutralize", "variant_counter"] - -## effects 键白名单(🔴 零裸 flat 同族护栏——机制轴禁无条件属性加项;键出域=加载致命) -## 全部条件触发/机制修正参数;cost_* 族=正负双修负向面 -const EFFECT_KEYS := ["value", "value2", "tier_value", "per_stack", "cap", "ap_cost", "chance", - "mult", "cost_mult", "cost_flat", "polarity", "spell_kind", "segment_cap", "discount_shift", - "trade_atk", "trade_hit", "shield_per_layer", "spike_mult", "layers_to_shield", - "neutralize_range", "counter_damage", "resource_kind", "rider_kind", "note"] +## trigger 行键(订阅行三字段:on/when?/do) +const TRIGGER_KEYS := ["on", "when", "do"] func validate() -> Array: var errs: Array = [] if rows.is_empty(): - return errs # 空表合法态(T1 基建先行——T2 起逐流派填行) + return errs # 空表合法态(T1 基建先行——逐流派填行) var seen := {} + # 流派扫雷覆盖聚合(🔴 红线①铁律——每流派 ≥1 行订阅扫雷侧事件) + var arch_mine := {} # archetype → bool for row in rows: var rid: String = row.get("feat_id", "") if rid == "": @@ -100,32 +62,27 @@ func validate() -> Array: var arch := String(row.get("archetype", "")) if not ARCHETYPES.has(arch): errs.append({"category": "enum", "message": "%s archetype 出域 %s" % [rid, str(ARCHETYPES)]}) - var hook := String(row.get("hook", "")) - if not HOOKS.has(hook): - errs.append({"category": "enum", "message": "%s hook 出域(挂点枚举封闭): %s" % [rid, hook]}) + if not row.has("triggers"): + errs.append({"category": "domain", "message": "%s 缺 triggers(订阅形态唯一合法行形)" % rid}) + elif EffectEngine.validate_trigger_row(rid, row, row.get("triggers", []) as Array, errs): + arch_mine[arch] = true for k in row.keys(): - if not ["feat_id", "display_name", "rarity", "archetype", "hook", "effects", "note"].has(String(k)): + if not ROW_KEYS.has(String(k)): errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s" % [rid, String(k)]}) - var fx: Dictionary = row.get("effects", {}) as Dictionary - for ek in fx.keys(): - if not EFFECT_KEYS.has(String(ek)): - errs.append({"category": "zero_flat", "message": "%s effects 键出白名单: %s(机制轴禁无条件属性加项)" % [rid, String(ek)]}) - # 🔴 红线① 扫雷触发点铁律:**有行的流派**各 ≥1 行 hook ∈ MINE_HOOKS(部分填充合法——T2-T6 - # 逐流派渐进;七流派全量覆盖完成度由批次一出口断言承载,加载闸不卡渐进填行) - var archs_with_rows := {} - for row in rows: - archs_with_rows[String(row.get("archetype", ""))] = true - for arch in archs_with_rows.keys(): - var covered := false - for row in rows: - if String(row.get("archetype", "")) == String(arch) and MINE_HOOKS.has(String(row.get("hook", ""))): - covered = true - break - if not covered: - errs.append({"category": "load_bearing", "message": "流派 %s 无扫雷结算触发点行(🔴 红线①铁律——每流派 ≥1 件挂扫雷端)" % String(arch)}) + # 🔴 红线① 扫雷触发点铁律:**有行的流派**各 ≥1 行订阅扫雷侧事件(MINE_EVENTS 同义校验) + for arch in archs_with_rows(): + if not bool(arch_mine.get(arch, false)): + errs.append({"category": "load_bearing", "message": "流派 %s 无扫雷结算触发点行(🔴 红线①铁律——每流派 ≥1 件订阅扫雷侧事件)" % String(arch)}) return errs +func archs_with_rows() -> Array: + var out := {} + for row in rows: + out[String(row.get("archetype", ""))] = true + return out.keys() + + func row_by_id(feat_id: String) -> Dictionary: for row in rows: if String(row.get("feat_id", "")) == feat_id: diff --git a/game/data/schemas/treasure_item_table.gd b/game/data/schemas/treasure_item_table.gd index 58d5f8fd..e4dff0dc 100644 --- a/game/data/schemas/treasure_item_table.gd +++ b/game/data/schemas/treasure_item_table.gd @@ -38,19 +38,23 @@ const DIMENSIONS := ["info", "risk", "action", "plan", "counter"] # 同调批扩 enum(迭代 11 第①批 T1)——新增九 kind 全部条件触发/机制修正类;mine_reveal/ap_grant 语义不变。 # cell_removal = 爆破工兵包中立移除格(T7 原位换装时启用——T6 影响评估前置);mine_hit_halve 系固定 # 「减半」语义不消费 magnitude(反弹 N 由 reflect_damage 行 magnitude 承载)。 +# 内容数据驱动扩展批 T2 挂账:主动件 kind 词族换代(reveal_tiles/preview/grant_ap/remove_mines/ +# refresh_pick——效果引擎 ACTIVE_KINDS 同名闭集),被动件 effect_kinds 词族随 T4 秘宝迁移退役 +# (passive 行改 triggers 事件订阅形态);旧词保留共存至迁移完成。 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"] + "reward_weight_bias", "bonus_refresh", + "reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"] 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 +# magnitude 消费面(≥1 校验):grant_ap 与同调 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"] + "ap_charge_safe", "reward_weight_bias", "bonus_refresh", "grant_ap"] # 🔴 零裸 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"] + "unlock_requires", "effective_rounds", "charge_n", "triggers", "desc"] func validate() -> Array: @@ -90,6 +94,13 @@ func validate() -> Array: 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)]}) + # triggers 事件订阅行(内容数据驱动扩展批 T2/T4——被动件换代形态;与 effect_kinds 互斥校验 + # 面=passive 行(换代单形态——效果行为只走订阅);active_charged 行两形合法并存 + # (effect_kinds=主动分发通道 + triggers=受理闸订阅,掘进蓄能先例)) + if row.has("triggers"): + if (row.get("effect_kinds", []) as Array).size() > 0 and String(row.get("form", "")) != "active_charged" and not bool(row.get("retired", false)): + errs.append({"category": "domain", "message": "%s triggers 与 effect_kinds 并存(换代单形态——被动件迁移期禁双挂)" % iid}) + EffectEngine.validate_trigger_row(iid, row, row.get("triggers", []) as Array, errs) 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]}) @@ -97,8 +108,8 @@ func validate() -> Array: # 退役/失义行:免 effect_kinds 非空与承重护栏校验(登记面保留,不入投放与 F2) continue # ---- 活行校验 ---- - if kinds.is_empty(): - errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred)" % iid}) + if kinds.is_empty() and not row.has("triggers"): + errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred,被动件应挂 triggers 订阅)" % iid}) var kind_seen := {} for k in kinds: var kind: String = String(k) @@ -123,8 +134,8 @@ func validate() -> Array: 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]}) + if not mag_consuming and mag != 0 and not row.has("triggers"): + errs.append({"category": "cross_field", "message": "%s 非 N 值 kind 活行 magnitude 应 0(trigger 行 magnitude=value_from 取数源豁免),实得 %d" % [iid, mag]}) # ---- 同调行约束(form 非空——装配面;过渡行 form="" 不校验)---- if form != "": if int(row.get("slot_cost", 1)) != 1: @@ -139,8 +150,8 @@ func validate() -> Array: 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]}) + if not kind_seen.has("ap_rounds_front") and eff_rounds != 0 and not row.has("triggers"): + errs.append({"category": "cross_field", "message": "%s 非 ap_rounds_front 行 effective_rounds 应 0(trigger 行=when value_from 取数源豁免),实得 %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]}) diff --git a/game/data/tables/feats.tres b/game/data/tables/feats.tres index 29d21854..cfe6a52f 100644 --- a/game/data/tables/feats.tres +++ b/game/data/tables/feats.tres @@ -9,266 +9,564 @@ rows = [{ "display_name": "奥术余烬", "rarity": "common", "archetype": "magic", -"hook": "hero_attack_rider", -"effects": { +"desc": "普攻附加必中伤害", +"triggers": [{ +"on": "attack_commit", +"do": { +"kind": "add_ticket_rider", "value": 1 } +}] }, { "feat_id": "mana_weaver", "display_name": "魔网织者", "rarity": "rare", "archetype": "magic", -"hook": "clamp_modifier", -"effects": { -"value": 1, -"polarity": "magic_vs_magic" +"desc": "克制面强化", +"triggers": [{ +"on": "clamp_check", +"when": [{ +"fact": "matchup", +"op": "eq", +"value": "magic_vs_magic" +}], +"do": { +"kind": "modify_ticket", +"delta": 1 } +}] }, { "feat_id": "azure_oath", "display_name": "湛蓝誓约", "rarity": "rare", "archetype": "magic", -"hook": "clamp_modifier", -"effects": { -"value": 1, -"polarity": "magic_vs_heavy" +"desc": "克制面强化", +"triggers": [{ +"on": "clamp_check", +"when": [{ +"fact": "matchup", +"op": "eq", +"value": "magic_vs_heavy" +}], +"do": { +"kind": "modify_ticket", +"delta": 1 } +}] }, { "feat_id": "mana_resonance", "display_name": "魔能共鸣", "rarity": "rarest", "archetype": "magic", -"hook": "first_dig_spell", -"effects": { +"desc": "首格追加奥术矢", +"triggers": [{ +"on": "dig_reveal", +"when": [{ +"fact": "is_first_dig_of_battle", +"op": "eq", +"value": true +}], +"do": { +"kind": "spawn_spell", "value": 2 } +}] }, { "feat_id": "mine_atlas", "display_name": "雷区图谱", "rarity": "common", "archetype": "flag", -"hook": "flag_resource", -"effects": { -"value": 1 +"desc": "标中雷额外得资源", +"triggers": [{ +"on": "flag_hit", +"do": { +"kind": "grant_material", +"value": 1, +"label": "(雷区图谱)" } +}] }, { "feat_id": "nest_hunter", "display_name": "猎巢人", "rarity": "rare", "archetype": "flag", -"hook": "nest_aura", -"effects": { -"value": 1 +"desc": "控巢攻击光环", +"triggers": [{ +"on": "attack_commit", +"do": { +"kind": "add_ticket_rider", +"value": 1, +"per": "nest_count" } +}] }, { "feat_id": "perfect_defense", "display_name": "完美布防", "rarity": "rare", "archetype": "flag", -"hook": "flag_all_hit", -"effects": { -"value": 2 +"desc": "全中标旗伤加倍", +"triggers": [{ +"on": "flag_settle", +"when": [{ +"fact": "flags_count", +"op": "gte", +"value": 1 +}], +"do": { +"kind": "modify_ticket", +"value": 2, +"mode": "flag_mult" } +}] }, { "feat_id": "war_banner", "display_name": "战旗军规", "rarity": "rarest", "archetype": "flag", -"hook": "flag_strike", -"effects": { -"value": 2 +"desc": "标中旗必中打敌", +"triggers": [{ +"on": "flag_settle", +"when": [{ +"fact": "flags_count", +"op": "gte", +"value": 1 +}], +"do": { +"kind": "open_ticket", +"value": 2, +"channel": "attack" } +}] }, { "feat_id": "pierce_shot", "display_name": "贯穿射击", "rarity": "rarest", "archetype": "pierce", -"hook": "kill_overflow", -"effects": { -"value": 1 +"desc": "击杀半额弹射", +"triggers": [{ +"on": "kill", +"do": { +"kind": "overflow", +"value": 50 } +}] }, { "feat_id": "chain_arrows", "display_name": "连珠矢", "rarity": "rare", "archetype": "pierce", -"hook": "dig_count_segments", -"effects": { -"value": 2, -"cap": 2 +"desc": "开格积穿刺矢", +"triggers": [{ +"on": "dig_reveal", +"do": { +"kind": "counter_add", +"counter_id": "chain_digs", +"value": 1, +"reset_at": "round_start" } }, { +"on": "attack_commit", +"do": { +"kind": "attack_segments", +"value": 2, +"cap": 2, +"per": "dig_streak", +"divisor": 3 +} +}] +}, { "feat_id": "weak_instinct", "display_name": "猎弱本能", "rarity": "rare", "archetype": "pierce", -"hook": "roll_discount", -"effects": { -"value": 1 +"desc": "打轻甲更易命中", +"triggers": [{ +"on": "clamp_check", +"when": [{ +"fact": "matchup", +"op": "ends_with", +"value": "vs_light" +}], +"do": { +"kind": "modify_ticket", +"value": 1, +"mode": "discount_shift" } +}] }, { "feat_id": "hunter_rapier", "display_name": "猎手刺剑", "rarity": "common", "archetype": "pierce", -"hook": "clamp_modifier", -"effects": { -"value": 1, -"polarity": "pierce_vs_light" +"desc": "克制面强化", +"triggers": [{ +"on": "clamp_check", +"when": [{ +"fact": "matchup", +"op": "eq", +"value": "pierce_vs_light" +}], +"do": { +"kind": "modify_ticket", +"delta": 1 } +}] }, { "feat_id": "miner_stubborn", "display_name": "矿工的执拗", "rarity": "common", "archetype": "normal", -"hook": "round_dig_bonus", -"effects": { +"desc": "开格越多打得越疼", +"triggers": [{ +"on": "dig_reveal", +"do": { +"kind": "counter_add", +"counter_id": "stubborn_digs", "value": 1, -"cap": 5 +"reset_at": "round_start" } }, { +"on": "mine_hit", +"do": { +"kind": "counter_add", +"counter_id": "stubborn_digs", +"set": 0 +} +}, { +"on": "attack_commit", +"do": { +"kind": "add_ticket_rider", +"value": 1, +"cap": 5, +"per": "dig_streak" +} +}] +}, { "feat_id": "miner_hammer", "display_name": "矿工重锤", "rarity": "common", "archetype": "normal", -"hook": "hit_trade", -"effects": { +"desc": "加伤换命中", +"triggers": [{ +"on": "attack_commit", +"do": { +"kind": "modify_ticket", "trade_atk": 25, "trade_hit": 2 } +}] }, { "feat_id": "focus_mind", "display_name": "专注", "rarity": "rare", "archetype": "normal", -"hook": "armor_roll_rewrite", -"effects": { -"value": 75 +"desc": "攻击恒75%折扣", +"triggers": [{ +"on": "armor_roll", +"do": { +"kind": "modify_ticket", +"value": 75, +"mode": "armor_rewrite" } +}] }, { "feat_id": "armor_crack", "display_name": "碎甲震击", "rarity": "rare", "archetype": "normal", -"hook": "heavy_debuff", -"effects": { -"value": 25 +"desc": "打重甲附加破防", +"triggers": [{ +"on": "attack_commit", +"when": [{ +"fact": "matchup", +"op": "ends_with", +"value": "vs_heavy" +}], +"do": { +"kind": "apply_status", +"status_id": "off_guard" } +}] }, { "feat_id": "greedy_lantern", "display_name": "贪婪矿灯", "rarity": "common", "archetype": "dig", -"hook": "dig_yield_mult", -"effects": { +"desc": "开格产出放大", +"triggers": [{ +"on": "dig_reveal", +"do": { +"kind": "yield_mult", "mult": 25, "cost_mult": 25 } +}] }, { "feat_id": "vein_intuition", "display_name": "矿脉直觉", "rarity": "rare", "archetype": "dig", -"hook": "dig_yield_mult", -"effects": { +"desc": "开格产出放大", +"triggers": [{ +"on": "dig_reveal", +"do": { +"kind": "counter_add", +"counter_id": "dig_combo", +"value": 1 +} +}, { +"on": "mine_hit", +"do": { +"kind": "counter_add", +"counter_id": "dig_combo", +"set": 0 +} +}, { +"on": "dig_reveal", +"do": { +"kind": "yield_mult", "mult": 10, "cap": 5 } +}] }, { "feat_id": "gambler_pact", "display_name": "赌徒的血签", "rarity": "rarest", "archetype": "dig", -"hook": "dig_yield_mult", -"effects": { +"desc": "开格产出放大", +"triggers": [{ +"on": "dig_reveal", +"do": { +"kind": "yield_mult", "mult": 50, "cost_mult": 100 } +}] }, { "feat_id": "deep_drill", "display_name": "深挖钻头", "rarity": "rare", "archetype": "dig", -"hook": "deep_dig", -"effects": { -"ap_cost": 1, -"cap": 4 +"desc": "开格2AP·产出双份", +"triggers": [{ +"on": "dig_paid", +"do": { +"kind": "counter_spend", +"counter_id": "deep_paid_round", +"value": 1, +"reset_at": "round_start", +"fallback": 1 } }, { +"on": "dig_reveal", +"when": [{ +"fact": "counter", +"id": "deep_paid_round", +"op": "gte", +"value": 1 +}, { +"fact": "counter", +"id": "deep_given_round", +"op": "lt_ref", +"ref": "deep_paid_round" +}], +"do": { +"kind": "grant_material", +"label": "(深挖)" +} +}, { +"on": "dig_reveal", +"when": [{ +"fact": "counter", +"id": "deep_paid_round", +"op": "gte", +"value": 1 +}, { +"fact": "counter", +"id": "deep_given_round", +"op": "lt_ref", +"ref": "deep_paid_round" +}], +"do": { +"kind": "counter_add", +"counter_id": "deep_given_round", +"value": 1 +} +}] +}, { "feat_id": "armory_prep", "display_name": "整备军备", "rarity": "rarest", "archetype": "charge", -"hook": "ap_carry", -"effects": { -"value": 1, -"cap": 3 +"desc": "剩AP转下轮攻击", +"triggers": [{ +"on": "round_end", +"do": { +"kind": "grant_ap", +"mode": "transfer" } }, { +"on": "attack_commit", +"do": { +"kind": "attack_bonus", +"value": 1, +"per": "charge_layers" +} +}] +}, { "feat_id": "iron_wall", "display_name": "铁壁整备", "rarity": "rare", "archetype": "charge", -"hook": "charge_shield", -"effects": { -"shield_per_layer": 1 +"desc": "蓄力转减伤", +"triggers": [{ +"on": "damage_taken", +"do": { +"kind": "shield", +"value": 1, +"per": "charge_layers" } +}] }, { "feat_id": "patient_burst", "display_name": "厚积薄发", "rarity": "rare", "archetype": "charge", -"hook": "charge_spike", -"effects": { -"value": 2 +"desc": "蓄满下轮爆发", +"triggers": [{ +"on": "attack_commit", +"when": [{ +"fact": "charge_layers", +"op": "gte", +"value": 3 +}], +"do": { +"kind": "modify_ticket", +"value": 2, +"mode": "spike" } +}] }, { "feat_id": "ley_harvest", "display_name": "地脉采撷", "rarity": "common", "archetype": "devour", -"hook": "terrain_refund", -"effects": { -"value": 1 +"desc": "吃地形伤返资源", +"triggers": [{ +"on": "damage_taken", +"when": [{ +"fact": "terrain_kind", +"op": "has" +}], +"do": { +"kind": "grant_material", +"label": "(地脉补偿)" } +}] }, { "feat_id": "thorn_weaver", "display_name": "荆棘编织者", "rarity": "rare", "archetype": "devour", -"hook": "terrain_dig_double", -"effects": { -"value": 1 +"desc": "挖伤害格双产出", +"triggers": [{ +"on": "damage_taken", +"when": [{ +"fact": "terrain_kind", +"op": "has" +}], +"do": { +"kind": "grant_material", +"label": "(荆棘编织)" } +}] }, { "feat_id": "purge_sapper", "display_name": "净化工兵", "rarity": "rare", "archetype": "devour", -"hook": "flag_neutralize", -"effects": { -"value": 1 +"desc": "标旗中和伤害格", +"triggers": [{ +"on": "flag_hit", +"when": [{ +"fact": "terrain_kind", +"op": "has" +}], +"do": { +"kind": "rewrite_board" } +}] }, { "feat_id": "magma_body", "display_name": "熔火之躯", "rarity": "rarest", "archetype": "devour", -"hook": "variant_counter", -"effects": { +"desc": "吃地形伤反打全体", +"triggers": [{ +"on": "damage_taken", +"when": [{ +"fact": "terrain_kind", +"op": "has" +}], +"do": { +"kind": "counter_add", +"counter_id": "magma_layers", "value": 1 } +}, { +"on": "round_end", +"when": [{ +"fact": "counter", +"id": "magma_layers", +"op": "gte", +"value": 1 +}], +"do": { +"kind": "open_ticket", +"value": 1, +"per": "magma_layers", +"channel": "blast" +} +}, { +"on": "round_end", +"when": [{ +"fact": "counter", +"id": "magma_layers", +"op": "gte", +"value": 1 +}], +"do": { +"kind": "counter_add", +"counter_id": "magma_layers", +"set": 0 +} +}] }] -; 双层 BD 实施批(2026-10-04):规则改写型特性表——行 = 特性定义 -; (feat_id/display_name/rarity/archetype/hook/effects)。 -; T2 魔法流四件(整合定稿 §2.3 S1):奥术余烬〔普通·rider+1 必中附加〕/魔网织者〔稀有·magic vs -; magic 克制罚修正〕/湛蓝誓约〔稀有·magic vs heavy 克制加成〕/魔能共鸣〔罕见锚点·rider+2+每战首次 -; 开格奥术矢——「每关首次」字面落地为「每战首次」台账披露待追认〕;magic 档一(≥2 件)rider+1 / -; 档二(≥4 件)rider 可暴击——引擎 tiers_for 承载。T3-T6 逐流派续填(flag 4/pierce 4/normal 4/ -; dig 4/charge 3/devour 4——整合定稿 §2.3 七流派的件面)。 -; 稀有度→投放档映射:common/rare → 稀有令牌档、rarest → 罕见令牌档(relic 永不入普通令牌档 -; ——「规则改写型特性带挂稀有/罕见档」追认 b 接线口径,普通档 70 张纯数值维持)。 -; 🔴 层级纪律:本表行永不入同调位(treasure_items)/局外树(T8 节点表)——规则改写型特性只走局内随机渠道。 -; 数值全 provisional 归 M-4 数值批;effects 键白名单封闭(零裸 flat 同族护栏)。 - +; 内容数据驱动扩展批(2026-10-06 T3):专长表换代——行 = triggers 事件订阅形态 +; (feat_id/display_name/rarity/archetype/desc/triggers——订阅哪个事件 on / 什么条件 when / 什么效果 do)。 +; 旧 hook/effects 挂点形态随本批退役(挂点枚举消亡:事件名+效果类型取代——任务书 R4 裁决)。 +; 23 挂点逐行移植映射=任务书 §五(等价性对拍底单);desc=文案表值化(呈现层读行,零硬编码)。 +; 效果参数值直接写 do 字典(数值单一事实源=本表;效果引擎 DO_KEYS 白名单封闭)。 +; 🔴 护栏全部保留:事件/效果/fact/算子四词表封闭(EffectEngine 常量)、行键白名单、悬空引用 +; 致命、零裸 flat 禁令、流派扫雷触发铁律(每流派 ≥1 行订阅扫雷侧事件——MINE_EVENTS 同义校验)。 +; 映射表对拍注记(T3 落地语义修正,行为等价优先——commit 留痕): +; - terrain_dig_double(荆棘编织者):映射表标 yield_mult(×2),现行为=第二份同格产出兑付 +; (grant_material),×2 乘区对 percent 行不等价——落 grant_material。 +; - flag_all_hit(完美布防):映射表标 yield_mult(旗伤乘区),现行为=旗伤单值倍增(战旗单据 +; 数值改写)——落 modify_ticket(mode:flag_mult)。 +; - 连珠矢/矿工的执拗 streak 取数:映射表 on:dig_reveal counter_add 形态与现行为「轮内踩雷全轮 +; 清零」不等价(逐格清零后重积≠全轮判定)——计数器行保留(扫雷铁律承载),等价执行读 +; attack_commit 载荷 dig_streak(战斗段全轮推导,与迁移前逐位一致)。 +; - 厚积薄发:映射表 on:charge_full(满层沿)→spike,现行为=满层持续状态(层数无消耗点,沿触发 +; 一次≠持续增强)——落 on:attack_commit + when(charge_layers ≥3) → modify_ticket(mode:spike), +; charge_full 事件仍埋点(R6 框架先行)。 +; - 完美布防:「全中」条件装不进 fact 封闭 9 项(需 hits>0 且 misses==0 双读数)——when 落 +; flags_count ≥1(可命中保守判定),全中闸=flag_settle 发射点施加面承载(载荷 flags_hit_all +; 仅供观测)——禁私开 fact(停机点纪律),查询面落法=代拍板白名单。 +; - 猎巢人/矿工的执拗:映射表标 modify_ticket/attack_bonus,现行为=必中附加通道(伤害控制器 +; hero_rider 后置于乘区与下限)——落 add_ticket_rider(per 取数载荷),乘区外恒定兑付逐位一致。 +; - 整备军备:现行为双效果(轮末 AP 转蓄力层 + 每层下轮首段攻 +1)——两行订阅: +; round_end grant_ap(mode:transfer) + attack_commit attack_bonus(per:charge_layers)。 +; 数值全 provisional 归 M-4 数值批;desc 文案=旧 summaries 表迁移(措辞零变化)。 diff --git a/game/data/tables/treasure_items.tres b/game/data/tables/treasure_items.tres index 3da8a3f6..4aaf2ec3 100644 --- a/game/data/tables/treasure_items.tres +++ b/game/data/tables/treasure_items.tres @@ -10,6 +10,7 @@ rows = [{ "item_id": "probe_dust", "display_name": "显影尘", "dimension_tags": ["info", "counter"], +"desc": "效果失义待重定", "effect_kinds": [], "target": "none", "magnitude": 0, @@ -19,6 +20,7 @@ rows = [{ "item_id": "sapper_order", "display_name": "工兵令", "dimension_tags": ["action"], +"desc": "AP +2", "effect_kinds": ["ap_grant"], "target": "none", "magnitude": 2, @@ -28,6 +30,7 @@ rows = [{ "item_id": "loot_compass", "display_name": "探宝罗盘", "dimension_tags": ["plan"], +"desc": "预读 3×3", "effect_kinds": ["reward_preview"], "target": "area_3x3", "magnitude": 0, @@ -37,6 +40,7 @@ rows = [{ "item_id": "nest_pendulum", "display_name": "探巢摆钟", "dimension_tags": ["risk"], +"desc": "真视 3×3", "effect_kinds": ["mine_reveal"], "target": "area_3x3", "magnitude": 0, @@ -48,6 +52,7 @@ rows = [{ "item_id": "catacomb_scroll", "display_name": "地穴全图卷", "dimension_tags": ["info", "plan"], +"desc": "效果失义待重定", "effect_kinds": [], "target": "board_wide", "magnitude": 0, @@ -57,6 +62,7 @@ rows = [{ "item_id": "blast_kit", "display_name": "爆破工兵包", "dimension_tags": ["action", "risk"], +"desc": "AP +4 真视 3×3 移除雷格", "effect_kinds": ["ap_grant", "mine_reveal", "cell_removal"], "target": "area_3x3", "magnitude": 4, @@ -68,47 +74,108 @@ rows = [{ "item_id": "venom_edge", "display_name": "淬毒刃纹", "dimension_tags": ["counter"], -"effect_kinds": ["venom_rider"], +"desc": "克制正面直伤+2", "target": "none", "magnitude": 2, "use_phase": "active_dig", "form": "passive", -"slot_cost": 1 +"slot_cost": 1, +"triggers": [{ +"on": "clamp_check", +"when": [{ +"fact": "matchup", +"op": "eq", +"value": "pierce_vs_ward" +}], +"do": { +"kind": "modify_ticket", +"delta_from": "magnitude", +"mode": "clamp_bonus" +} +}, { +"on": "clamp_check", +"when": [{ +"fact": "matchup", +"op": "eq", +"value": "magic_vs_heavy" +}], +"do": { +"kind": "modify_ticket", +"delta_from": "magnitude", +"mode": "clamp_bonus" +} +}] }, { "item_id": "flag_burst", "display_name": "标中殉爆", "dimension_tags": ["action"], -"effect_kinds": ["flag_burst"], +"desc": "每标中一旗全体敌受2", "target": "none", "magnitude": 2, "use_phase": "active_dig", "form": "passive", -"slot_cost": 1 +"slot_cost": 1, +"triggers": [{ +"on": "flag_hit", +"do": { +"kind": "open_ticket", +"value_from": "magnitude", +"channel": "blast" +} +}] }, { "item_id": "nest_counter", "display_name": "惊巢反制", "dimension_tags": ["risk"], -"effect_kinds": ["mine_hit_halve", "reflect_damage"], +"desc": "踩雷惩罚减半+反弹2", "target": "none", "magnitude": 2, "use_phase": "active_dig", "form": "passive", -"slot_cost": 1 +"slot_cost": 1, +"triggers": [{ +"on": "mine_hit", +"do": { +"kind": "damage_mult", +"value": 50 +} +}, { +"on": "mine_hit", +"do": { +"kind": "open_ticket", +"value_from": "magnitude", +"channel": "reflect" +} +}] }, { "item_id": "dawn_order", "display_name": "工兵晨令", "dimension_tags": ["action"], -"effect_kinds": ["ap_rounds_front"], +"desc": "前3轮每轮AP+1", "target": "none", "magnitude": 1, "use_phase": "active_dig", "form": "passive", "slot_cost": 1, -"effective_rounds": 3 +"effective_rounds": 3, +"triggers": [{ +"on": "round_start", +"when": [{ +"fact": "round_index", +"op": "lte", +"value_from": "effective_rounds" +}], +"do": { +"kind": "grant_ap", +"mode": "queue", +"value_from": "magnitude" +} +}] }, { "item_id": "dig_charge", "display_name": "掘进蓄能", "dimension_tags": ["action"], +"desc": "积点换AP+1", "effect_kinds": ["ap_charge_safe"], "target": "none", "magnitude": 1, @@ -116,21 +183,37 @@ rows = [{ "form": "active_charged", "slot_cost": 1, "charges_per_adventure": 2, -"charge_n": 3 +"charge_n": 3, +"triggers": [{ +"on": "treasure_used", +"do": { +"kind": "counter_spend", +"counter_id": "dig_charge_bank", +"value": 1 +} +}] }, { "item_id": "luck_coin", "display_name": "偏运硬币", "dimension_tags": ["risk"], -"effect_kinds": ["reward_weight_bias"], +"desc": "稀有资源权重+2", "target": "none", "magnitude": 2, "use_phase": "active_dig", "form": "passive", -"slot_cost": 1 +"slot_cost": 1, +"triggers": [{ +"on": "reward_roll", +"do": { +"kind": "bias_rewards", +"value_from": "magnitude" +} +}] }, { "item_id": "pick_refresh", "display_name": "三选一刷新件", "dimension_tags": ["info"], +"desc": "三选一刷新+1", "effect_kinds": ["bonus_refresh"], "target": "none", "magnitude": 1, diff --git a/game/presentation/battle_narrator.gd b/game/presentation/battle_narrator.gd index f3b0b273..32e612b6 100644 --- a/game/presentation/battle_narrator.gd +++ b/game/presentation/battle_narrator.gd @@ -131,40 +131,16 @@ static func bonus_result_text(result: Dictionary) -> String: return "已选择:%s\n其余选项已丢弃(不可反悔)" % name -# 特性卡片文案(迭代 12 T1;2026-10-04 呈现补强——第三行=hook 效果摘要,用户反馈「不知道件是 -# 干嘛的」缺口承载;摘要=静态映射表 provisional 文案归代拍板白名单「件文案微调」) +# 特性卡片文案(迭代 12 T1;2026-10-04 呈现补强——第三行=效果摘要,用户反馈「不知道件是 +# 干嘛的」缺口承载。内容数据驱动扩展批 T3 文案表值化:摘要=数据行 desc 字段,零硬编码映射—— +# 新件写行即有名有姓,措辞与旧 summaries 表逐字一致) static func feat_card_text(row: Dictionary) -> String: if row.is_empty() or String(row.get("feat_id", "")) == "": return "特性\n???" var arch_names := {"magic": "魔法", "pierce": "穿刺", "normal": "普攻", "dig": "开格", "flag": "标旗", "charge": "蓄力", "devour": "吞噬"} var arch := String(arch_names.get(String(row.get("archetype", "")), "流派")) - var summaries := { - "hero_attack_rider": "普攻附加必中伤害", - "clamp_modifier": "克制面强化", - "first_dig_spell": "首格追加奥术矢", - "kill_overflow": "击杀半额弹射", - "dig_count_segments": "开格积穿刺矢", - "roll_discount": "打轻甲更易命中", - "round_dig_bonus": "开格越多打得越疼", - "hit_trade": "加伤换命中", - "armor_roll_rewrite": "攻击恒75%折扣", - "heavy_debuff": "打重甲附加破防", - "dig_yield_mult": "开格产出放大", - "deep_dig": "开格2AP·产出双份", - "flag_resource": "标中雷额外得资源", - "nest_aura": "控巢攻击光环", - "flag_all_hit": "全中标旗伤加倍", - "flag_strike": "标中旗必中打敌", - "ap_carry": "剩AP转下轮攻击", - "charge_shield": "蓄力转减伤", - "charge_spike": "蓄满下轮爆发", - "terrain_refund": "吃地形伤返资源", - "terrain_dig_double": "挖伤害格双产出", - "flag_neutralize": "标旗中和伤害格", - "variant_counter": "吃地形伤反打全体", - } - var summary := String(summaries.get(String(row.get("hook", "")), "")) + var summary := String(row.get("desc", "")) if summary != "": return "%s · %s\n%s" % [String(row.get("display_name", row.get("feat_id", ""))), arch, summary] return "特性 · %s\n%s" % [arch, String(row.get("display_name", row.get("feat_id", "")))] diff --git a/game/presentation/curio_interact_controller.gd b/game/presentation/curio_interact_controller.gd index 1657f04c..4cf3b92a 100644 --- a/game/presentation/curio_interact_controller.gd +++ b/game/presentation/curio_interact_controller.gd @@ -191,27 +191,19 @@ func _curio_card_texts(candidates: Array, treasure_detail: Array = []) -> Array: return out -# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案) +# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案)。 +# 内容数据驱动扩展批 T4 文案表值化:效果摘要=数据行 desc 字段直读(零 kind 硬编码映射—— +# 新件写行即有名有姓) func _treasure_card_line(iid: String) -> String: if iid == "": return "" var row: Dictionary = db.treasure_item_row(iid) if row.is_empty(): return "" - var parts: Array[String] = [] - for k in (row.get("effect_kinds", []) as Array): - if String(k) == "mine_reveal": - parts.append("真视") - elif String(k) == "reward_preview": - parts.append("预读") - elif String(k) == "ap_grant": - parts.append("AP +%d" % int(row.get("magnitude", 0))) - var tgt := String(row.get("target", "none")) - if tgt == "area_3x3": - parts.append("3×3") - elif tgt == "board_wide": - parts.append("全图") - return "%s · %s" % [String(row.get("display_name", iid)), " ".join(parts)] + var desc := String(row.get("desc", "")) + if desc != "": + return "%s · %s" % [String(row.get("display_name", iid)), desc] + return String(row.get("display_name", iid)) # 三选一刷新(迭代 11 第①批 T5b X/Y 双源 + 2026-10-06 用户令对齐商店 MC-1——免费位用尽转 GP 付费档: diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 5239d316..353c03e0 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -619,17 +619,23 @@ func _smoke_feats() -> void: rarest_tokens += 1 check(feat_tokens == 27 and rarest_tokens == 6 and a_r0._curio_deck.size() == 125, "规则改写型特性带入池:27 件(rare 档 21 + rarest 锚点 6)——牌库 98+27=125") - check(_db.feats.validate().is_empty(), "feat_table.validate 零违例(有行流派扫雷端铁律——magic first_dig_spell 在案)") - # ② 档位激活引擎(纯函数直测——构造表 4 件 magic + 1 件 flag + 1 件 rarest 锚点) + check(_db.feats.validate().is_empty(), "feat_table.validate 零违例(有行流派扫雷端铁律——magic 魔能共鸣 dig_reveal 订阅在案)") + # ② 档位激活引擎(纯函数直测——构造表 4 件 magic + 1 件 flag;内容数据驱动扩展批 T3 断言面随迁: + # 行=triggers 事件订阅形态,挂点投影断言换代=效果引擎订阅装载序〔feats 表序〕) var rt := FeatTable.new() rt.rows = [ - {"feat_id": "m1", "display_name": "魔一", "rarity": "common", "archetype": "magic", "hook": "hero_attack_rider", "effects": {"value": 1}}, - {"feat_id": "m2", "display_name": "魔二", "rarity": "common", "archetype": "magic", "hook": "first_dig_spell", "effects": {"value": 1}}, - {"feat_id": "m3", "display_name": "魔三", "rarity": "rare", "archetype": "magic", "hook": "clamp_modifier", "effects": {"value": 1}}, - {"feat_id": "m4", "display_name": "魔四", "rarity": "rare", "archetype": "magic", "hook": "hero_attack_rider", "effects": {"tier_value": 2}}, - {"feat_id": "f1", "display_name": "旗一", "rarity": "common", "archetype": "flag", "hook": "flag_resource", "effects": {"value": 1}}, + {"feat_id": "m1", "display_name": "魔一", "rarity": "common", "archetype": "magic", "desc": "测试 rider", + "triggers": [{"on": "attack_commit", "do": {"kind": "add_ticket_rider", "value": 1}}]}, + {"feat_id": "m2", "display_name": "魔二", "rarity": "common", "archetype": "magic", "desc": "测试奥术矢", + "triggers": [{"on": "dig_reveal", "do": {"kind": "spawn_spell", "value": 1}}]}, + {"feat_id": "m3", "display_name": "魔三", "rarity": "rare", "archetype": "magic", "desc": "测试克制", + "triggers": [{"on": "clamp_check", "when": [{"fact": "matchup", "op": "eq", "value": "magic_vs_magic"}], "do": {"kind": "modify_ticket", "delta": 1}}]}, + {"feat_id": "m4", "display_name": "魔四", "rarity": "rare", "archetype": "magic", "desc": "测试加值", + "triggers": [{"on": "attack_commit", "do": {"kind": "attack_bonus", "value": 2}}]}, + {"feat_id": "f1", "display_name": "旗一", "rarity": "common", "archetype": "flag", "desc": "测试图谱", + "triggers": [{"on": "flag_hit", "do": {"kind": "grant_material", "value": 1}}]}, ] - check(rt.validate().is_empty(), "构造 relic 表 validate 零违例(magic 4 件含 first_dig_spell 扫雷端——红线①铁律过)") + check(rt.validate().is_empty(), "构造 relic 表 validate 零违例(magic 4 件含 dig_reveal 扫雷端订阅——红线①铁律过)") var tiers := FeatsEngine.tiers_for(["m1", "m2", "m3", "m4", "f1"], rt) check(int(tiers.get("magic", -1)) == 2 and int(tiers.get("flag", -1)) == 0, "档位激活:magic 4 件→档二 · flag 1 件→散件档 0(集 2 档一/4 档二——以撒套装/小骨铭文先例)") @@ -637,9 +643,6 @@ func _smoke_feats() -> void: "成型判读:magic 成型(tier≥1)· flag 未成型(散件单体保底——档位只质变不启动)") var t2 := FeatsEngine.tiers_for(["m1", "m2"], rt) check(int(t2.get("magic", -1)) == 1, "档位激活:2 件→档一(同集合同档位确定性——MC 逐位可复放承载)") - var hooks := FeatsEngine.hooks_for(["m1", "f1"], rt) - check(hooks.size() == 2 and String(hooks[0].get("hook", "")) == "hero_attack_rider", - "挂点投影:表序稳定(同集合→同投影)") # ③ 规则改写型特性带接线(临时表注入——relic 令牌入牌库/兑付入持有集/踢池/断点往返;测试尾还原现行表) var live_rows: FeatTable = _db.feats _db.feats = rt @@ -721,56 +724,121 @@ func _smoke_feats() -> void: check(bp_deck_m3 == 0, "断点余库:已兑付 relic 令牌逐枚还原(m3 零残留——续玩不重复获得)") sm_r.clear_breakpoint() _db.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知) - # ④ T2 魔法流求值面(现行表 magic 四件——flags 组装直测;rider 落地行为=伤害控制器消费面, - # 既有战斗冒烟全绿=feat_flags 空 dict 零扰动实证;全链分岔读数归 T7 MC) + # ④ 魔法流订阅面(内容数据驱动扩展批 T3 断言面随迁——_feat_flags_for 挂点求值退役: + # 求值断言=效果引擎 rebuild+dispatch 命中面;落地行为=发射点施加面(本段下方各事件直测), + # 全链分岔读数归 T6 MC 对拍) var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(73)) - orch._feat_flags_for(["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance"]) - var rf: Dictionary = orch._damage.feat_flags - check(int(rf.get("rider_flat", -1)) == 2 and bool(rf.get("rider_crit", false)), - "魔法流求值:rider_flat=余烬1+档一(4件)+1=2 · 档二 rider 可暴击(共鸣 rider 提升以奥术矢+档位承载——锚点双效果 provisional 披露)") - check(int(rf.get("clamp_magic_magic", -1)) == 1 and int(rf.get("clamp_magic_heavy", -1)) == 1 - and bool(rf.get("first_dig_spell", false)) and int(rf.get("spell_value", -1)) == 2, - "魔法流求值:魔网织者 magic-vs-magic 罚修正 +1 · 湛蓝誓约 magic-vs-heavy +1 · 魔能共鸣奥术矢 value 2") - # ⑤ T3 标旗流求值面(现行表 magic 4+flag 4——混合持有:flag 档一 4 件成型) - orch._feat_flags_for(["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance", - "mine_atlas", "nest_hunter", "perfect_defense", "war_banner"]) - var rf8: Dictionary = orch._damage.feat_flags - check(int(rf8.get("flag_resource_extra", -1)) == 1 and int(rf8.get("nest_aura_value", -1)) == 1 - and int(rf8.get("flag_all_hit_mult", -1)) == 2 and int(rf8.get("flag_strike_value", -1)) == 2, - "标旗流求值:雷区图谱额外资源 1 · 猎巢人光环 +1/巢 · 完美布防 ×2 · 战旗军规旗伤 2(复用 board_typed 零新配方)") - # ⑥ T4 穿刺/普攻流求值面(全 16 件持有——四流派档位成型) - orch._feat_flags_for(["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance", - "mine_atlas", "nest_hunter", "perfect_defense", "war_banner", + var magic4: Array = ["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance"] + orch._fx.rebuild(magic4, [], []) + var ac4: Array = orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "magic_vs_magic", "dig_streak": 0, "nest_count": 0}) + check(orch._hits_of(ac4, "add_ticket_rider").size() == 1 + and int((orch._hits_of(ac4, "add_ticket_rider")[0] as Dictionary).get("value", -1)) == 1, + "魔法流订阅:奥术余烬 attack_commit→add_ticket_rider value 1(必中附加通道)") + check(int((orch._hits_of(orch._fx_emit("clamp_check", {"matchup": "magic_vs_magic"}), "modify_ticket")[0] as Dictionary).get("delta", -1)) == 1 + and int((orch._hits_of(orch._fx_emit("clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket")[0] as Dictionary).get("delta", -1)) == 1, + "魔法流订阅:魔网织者 magic-vs-magic +1 · 湛蓝誓约 magic-vs-heavy +1(clamp_check modify 窗口)") + var spell4: Array = orch._fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": true, "terrain_kind": []}) + check(orch._hits_of(spell4, "spawn_spell").size() == 1 + and int((orch._hits_of(spell4, "spawn_spell")[0] as Dictionary).get("value", -1)) == 2, + "魔法流订阅:魔能共鸣每战首次开格→spawn_spell value 2(奥术矢)") + check(int(FeatsEngine.tiers_for(magic4, _db.feats).get("magic", -1)) == 2, + "魔法流档位:magic 4 件→档二(rider 可暴击——begin_battle 注伤害控制器两运行态)") + # ⑤ 标旗流订阅面(magic 4+flag 4——混合持有:flag 档一成型) + orch._fx.rebuild(magic4 + ["mine_atlas", "nest_hunter", "perfect_defense", "war_banner"], [], []) + check(orch._hits_of(orch._fx_emit("flag_hit", {"cell": Vector2i(1, 1), "round": 1, "terrain_kind": []}), "grant_material").size() == 1, + "标旗流订阅:雷区图谱 flag_hit→grant_material(标中雷额外资源)") + var nh5: Array = orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 3}) + var nh_hits: Array = orch._hits_of(nh5, "add_ticket_rider") + check(nh_hits.size() == 2 and String((nh_hits[1] as Dictionary).get("per", "")) == "nest_count", + "标旗流订阅:猎巢人 attack_commit→add_ticket_rider per nest_count(控巢光环 rider 通道)") + var fs5: Array = orch._fx_emit("flag_settle", {"flags_hit_all": true, "flags_count": 2, "round": 1}) + check(orch._hits_of(fs5, "open_ticket").size() == 1 + and int((orch._hits_of(fs5, "open_ticket")[0] as Dictionary).get("value", -1)) == 2 + and orch._hits_of(fs5, "modify_ticket").size() == 1, + "标旗流订阅:战旗军规 flag_settle→open_ticket value 2(attack 通道自开单)· 完美布防 flag_mult ×2") + check(orch._fx_emit("flag_settle", {"flags_hit_all": false, "flags_count": 0, "round": 1}).is_empty(), + "标旗流订阅:零标中旗→战旗/完美布防零命中(when flags_count 封闭判定)") + # ⑥ 穿刺/普攻流订阅面(16 件持有——四流派档位成型) + var ids16: Array = magic4 + ["mine_atlas", "nest_hunter", "perfect_defense", "war_banner", "pierce_shot", "chain_arrows", "weak_instinct", "hunter_rapier", - "miner_stubborn", "miner_hammer", "focus_mind", "armor_crack"]) - var rf16: Dictionary = orch._damage.feat_flags - check(bool(rf16.get("kill_overflow", false)) and int(rf16.get("chain_value", -1)) == 2 - and int(rf16.get("discount_shift", -1)) == 1 and int(rf16.get("clamp_pierce_light", -1)) == 1, - "穿刺流求值:贯穿射击弹射旗 · 连珠矢段值 2 上限 2 · 猎弱本能折扣改善 1 档 · 猎手刺剑 pierce-vs-light +1") - check(int(rf16.get("round_dig_value", -1)) == 1 and int(rf16.get("hit_trade_atk", -1)) == 25 - and int(rf16.get("hit_trade_hit", -1)) == 2 and int(rf16.get("armor_roll_rewrite", -1)) == 75 - and int(rf16.get("crack_value", -1)) == 25, - "普攻流求值:执拗 +1/格上限 5 · 重锤交换 +25%/−2AC · 专注恒 75% 免掷骰 · 碎甲震击 off_guard 授予旗") - check(bool(rf16.get("rider_crit", false)) and bool(rf16.get("rider_crit", false)), + "miner_stubborn", "miner_hammer", "focus_mind", "armor_crack"] + orch._fx.rebuild(ids16, [], []) + check(orch._hits_of(orch._fx_emit("kill", {"target": "敌", "overflow": 10}), "overflow").size() == 1, + "穿刺流订阅:贯穿射击 kill→overflow(击杀弹射旗)") + var dr6: Array = orch._fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": false, "terrain_kind": []}) + check(orch._hits_of(dr6, "counter_add").size() == 2, + "穿刺/普攻流订阅:连珠矢+执拗 dig_reveal→counter_add(扫雷侧事件铁律承载——chain_digs/stubborn_digs)") + var ac6: Array = orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "pierce_vs_light", "dig_streak": 9, "nest_count": 0}) + var seg6: Array = orch._hits_of(ac6, "attack_segments") + check(seg6.size() == 1 and int((seg6[0] as Dictionary).get("value", -1)) == 2 + and int((seg6[0] as Dictionary).get("cap", -1)) == 2 and int((seg6[0] as Dictionary).get("divisor", -1)) == 3, + "穿刺流订阅:连珠矢 attack_commit→attack_segments(段值 2 上限 2 每 3 格 +1 段)") + var clamp6: Array = orch._fx_emit("clamp_check", {"matchup": "pierce_vs_light"}) + check(orch._hits_of(clamp6, "modify_ticket").size() == 2, + "穿刺流订阅:猎弱本能 ends_with vs_light→discount_shift · 猎手刺剑 pierce_vs_light→delta +1(同一事件双订阅按声明序)") + check(orch._hits_of(orch._fx_emit("armor_roll", {"target_ac": 5, "roll": 3}), "modify_ticket").size() == 1, + "普攻流订阅:专注 armor_roll→modify armor_rewrite 75(免掷恒折扣)") + var ac6n: Array = orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "normal_vs_heavy", "dig_streak": 0, "nest_count": 0}) + check(orch._hits_of(ac6n, "apply_status").size() == 1 + and String((orch._hits_of(ac6n, "apply_status")[0] as Dictionary).get("status_id", "")) == "off_guard", + "普攻流订阅:碎甲震击 ends_with vs_heavy→apply_status off_guard(复用零新状态行)") + var ham6: Array = orch._hits_of(ac6, "modify_ticket") + var has_trade := false + for h in ham6: + if int((h as Dictionary).get("trade_atk", 0)) == 25 and int((h as Dictionary).get("trade_hit", 0)) == 2: + has_trade = true + check(has_trade, "普攻流订阅:矿工重锤 attack_commit→modify trade_atk 25/trade_hit 2(命中交换)") + check(int(FeatsEngine.tiers_for(ids16, _db.feats).get("pierce", -1)) == 2 + and int(FeatsEngine.tiers_for(ids16, _db.feats).get("normal", -1)) == 2, "档位质变:magic/pierce/normal/flag 四流派档二(16 件混合——同标签 ≥4 判档二确定性)") - # ⑦ T5 开格/蓄力流求值面(追加七件) - var ids23: Array = ["arcane_ember", "mana_weaver", "azure_oath", "mana_resonance", - "mine_atlas", "nest_hunter", "perfect_defense", "war_banner", - "pierce_shot", "chain_arrows", "weak_instinct", "hunter_rapier", - "miner_stubborn", "miner_hammer", "focus_mind", "armor_crack", - "greedy_lantern", "vein_intuition", "gambler_pact", "deep_drill", - "armory_prep", "iron_wall", "patient_burst"] - orch._feat_flags_for(ids23) - var rf23: Dictionary = orch._damage.feat_flags - check(int(rf23.get("dig_yield_pos_mult", -1)) == 85 and int(rf23.get("dig_yield_neg_mult", -1)) == 125 - and int(rf23.get("combo_cap", -1)) == 5, - "开格流求值:正向乘区 25+10+50=85% · 负向加重 25+100=125% · 直觉连击上限 5(同 hook 累加——三件同持)") - check(int(rf23.get("deep_dig_cost", -1)) == 1 and int(rf23.get("deep_dig_cap", -1)) == 4 - and int(rf23.get("ap_carry_value", -1)) == 1 and int(rf23.get("shield_per_layer", -1)) == 1 - and int(rf23.get("spike_mult", -1)) == 2, - "深挖钻头=开掘 +1 AP/每 4 格深层产出(R4 甲非信息税对价)· 蓄力三件=层值 1/减伤 1/层/满层 ×2") - check(orch.action_economy.feat_ap_carry and orch.action_economy.feat_dig_extra == 1, - "蓄力/钻头 action_economy 注入(ap_carry 改写停手转存 + 付费开掘对价注入——关着 false/0 零扰动)") + # ⑦ 开格/蓄力流订阅面(追加七件) + var ids23: Array = ids16.duplicate() + ids23.append_array(["greedy_lantern", "vein_intuition", "gambler_pact", "deep_drill", + "armory_prep", "iron_wall", "patient_burst"]) + orch._fx.rebuild(ids23, [], []) + var pos_m := 0 + var neg_m := 0 + var combo_cap := -1 + for h in orch._fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": false, "terrain_kind": []}): + if String(h.get("kind", "")) != "yield_mult": + continue + pos_m += int(h.get("mult", 0)) + neg_m += int(h.get("cost_mult", 0)) + if int(h.get("cap", 0)) > 0: + combo_cap = int(h.cap) + check(pos_m == 85 and neg_m == 125 and combo_cap == 5, + "开格流订阅:正向乘区 25+10+50=85% · 负向加重 25+100=125% · 直觉连击上限 5(dig_reveal yield_mult 乘法族三件同持收集)") + var dd7: Array = orch._fx_emit("dig_paid", {"ap_cost": 1, "ap_surplus": 1}) + check(orch._hits_of(dd7, "counter_spend").size() == 1 + and bool((orch._hits_of(dd7, "counter_spend")[0] as Dictionary).has("fallback")), + "开格流订阅:深挖钻头 dig_paid→counter_spend 对价(fallback 降级声明——AP 不足只付基础 1)") + var re7: Array = orch._fx_emit("round_end", {"round": 1}) + check(orch._hits_of(re7, "grant_ap").size() == 1 + and String((orch._hits_of(re7, "grant_ap")[0] as Dictionary).get("mode", "")) == "transfer", + "蓄力流订阅:整备军备 round_end→grant_ap transfer(剩 AP 转蓄力层——finish_round_stop 定夺)") + var ab7: Array = orch._hits_of(orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus") + check(ab7.size() == 1 and String((ab7[0] as Dictionary).get("per", "")) == "charge_layers", + "蓄力流订阅:整备军备 attack_commit→attack_bonus per charge_layers(每层下轮首段 +1)") + orch.action_economy._charge_layers = 2 + var sp7: Array = orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "normal_vs_light", "dig_streak": 0, "nest_count": 0}) + var spike7_off: Array = orch._hits_of(sp7, "modify_ticket") + var spike_off_n := 0 + for h in spike7_off: + if String((h as Dictionary).get("mode", "")) == "spike": + spike_off_n += 1 + check(spike_off_n == 0, + "蓄力流订阅:厚积薄发未满层(charge_layers 2<3)→零命中(when 封闭判定——满层持续状态口径)") + orch.action_economy._charge_layers = 3 + var sp7b: Array = orch._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "normal_vs_light", "dig_streak": 0, "nest_count": 0}) + var spike7: Array = [] + for h in orch._hits_of(sp7b, "modify_ticket"): + if String((h as Dictionary).get("mode", "")) == "spike": + spike7.append(h) + check(spike7.size() == 1 and int((spike7[0] as Dictionary).get("value", -1)) == 2, + "蓄力流订阅:厚积薄发满层→modify spike ×2(discount 恒全额+基值倍乘)") + orch.action_economy._charge_layers = 0 + check(orch._hits_of(orch._fx_emit("damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1}), "shield").size() == 1, + "蓄力流订阅:铁壁整备 damage_taken→shield per charge_layers(蓄力转减伤)") # ⑦c 特性持有呈现段(2026-10-04 用户反馈补齐——C 区「特性」节:聚合行+成型档位;空态隐藏) var panel = load("res://presentation/views/hero_info_panel_view.gd").new() panel.set_feat_holdings([]) @@ -778,30 +846,46 @@ func _smoke_feats() -> void: panel.set_feat_holdings(["魔法 ×2 · 档一", "开格 ×1 · 散件"]) check(panel._feat_section.visible and panel._feat_list.get_child_count() == 2, "特性段持有呈现(聚合行+成型档位常驻可读——「局内奖励选了什么看不到」缺口承载)") - # ⑦b 深挖钻头付费开掘回归(2026-10-04 试玩缺陷修复——对价挂付费侧):AP=1 降级照常开格 - # (用户实测「AP=1 无法开格只能标旗」的回归闸);AP=2 付全价积付费计数(深层产出配额取数面)。 + check(String(load("res://presentation/battle_narrator.gd").feat_card_text({"feat_id": "arcane_ember", "display_name": "奥术余烬", "archetype": "magic", "desc": "普攻附加必中伤害"})).contains("普攻附加必中伤害"), + "特性卡文案表值化:desc 行字段直读(T3 文案入表——零硬编码映射)") + # ⑦b 深挖钻头付费开掘回归(2026-10-04 试玩缺陷修复口径,T3 对价面换代=dig_paid 订阅施加): + # AP=1 降级照常开格(fallback 只付基础 1 不积计数);AP=2 付全价积付费计数(深层产出配额取数面)。 var ae_dd = load("res://core/action_economy.gd").new(_db) - ae_dd.feat_dig_extra = 1 + var fx_dd = EffectEngine.new(_db) + fx_dd.rebuild(["deep_drill"], [], []) + ae_dd.set_fx(fx_dd) ae_dd._phase = 1 if ae_dd.Phase.ACTIVE == 1 else ae_dd.Phase.ACTIVE # white-box 相位(冒烟专用) ae_dd._ap = 1 - check(ae_dd.request_dig(0, false) and ae_dd.deep_paid_count == 0, - "钻头降级:AP=1 照常开格(只付基础 1 不积付费——持有钻头永不锁死挖格,散件保底 B-2)") + check(ae_dd.request_dig(0, false) and fx_dd.counter("deep_paid_round") == 0, + "钻头降级:AP=1 照常开格(fallback 只付基础 1 不积付费——持有钻头永不锁死挖格,散件保底 B-2)") ae_dd._ap = 2 ae_dd._phase = ae_dd.Phase.ACTIVE # 首挖 AP 归 0 已翻轮(ap_exhausted 仲裁)——白盒复位续测 - check(ae_dd.request_dig(0, false) and ae_dd.deep_paid_count == 1, - "钻头付费开掘:AP=2 付全价(基础 1+对价 1——deep_paid_count 积数=深层产出配额取数面)") - # ⑧ T6 吞噬流求值面(27 件全量——七流派档位成型) + check(ae_dd.request_dig(0, false) and fx_dd.counter("deep_paid_round") == 1, + "钻头付费开掘:AP=2 付全价(基础 1+对价 1——deep_paid_round 计数器=授予段追加产出配额取数面)") + # ⑧ 吞噬流订阅面(27 件全量——七流派档位成型) var ids27: Array = ids23.duplicate() ids27.append_array(["ley_harvest", "thorn_weaver", "purge_sapper", "magma_body"]) - orch._feat_flags_for(ids27) - var rf27: Dictionary = orch._damage.feat_flags - check(int(rf27.get("terrain_refund", -1)) == 1 and int(rf27.get("terrain_dig_double", -1)) == 1 - and int(rf27.get("flag_neutralize", -1)) == 1 and int(rf27.get("variant_counter_value", -1)) == 1, - "吞噬流求值:地脉采撷补偿 1 · 荆棘编织双产出 · 净化工兵中和旗(🔴 仅伤害型——过滤在消费面)· 熔火反噬 1/层") + orch._fx.rebuild(ids27, [], []) + var dt8: Array = orch._fx_emit("damage_taken", {"kind": "terrain", "amount": 3, "source": "thorn_field", "round": 1, "terrain_kind": "thorn_field"}) + check(orch._hits_of(dt8, "grant_material").size() == 2 and orch._hits_of(dt8, "counter_add").size() == 1, + "吞噬流订阅:地脉采撷+荆棘编织 damage_taken(地形伤)→grant_material ×2 · 熔火之躯→counter_add 积层") + check(orch._hits_of(orch._fx_emit("flag_hit", {"cell": Vector2i(1, 1), "round": 1, "terrain_kind": ["thorn_field"]}), "rewrite_board").size() == 1 + and orch._hits_of(orch._fx_emit("flag_hit", {"cell": Vector2i(1, 1), "round": 1, "terrain_kind": []}), "rewrite_board").is_empty(), + "吞噬流订阅:净化工兵 flag_hit 邻伤害型地形→rewrite_board · 无伤害型邻格零命中(🔴 仅伤害型——terrain_kind has 在场判定)") + orch._fx.counter_add("magma_layers", 2, false) + var re8: Array = orch._fx_emit("round_end", {"round": 1}) + check(orch._hits_of(re8, "open_ticket").size() == 1 + and int((orch._hits_of(re8, "open_ticket")[0] as Dictionary).get("value", -1)) == 1 + and String((orch._hits_of(re8, "open_ticket")[0] as Dictionary).get("per", "")) == "magma_layers", + "吞噬流订阅:熔火之躯 round_end→open_ticket per magma_layers(受地形伤积层轮末反打)") + check(orch._hits_of(re8, "counter_add").size() == 1 + and (orch._hits_of(re8, "counter_add")[0] as Dictionary).has("set"), + "吞噬流订阅:熔火之躯反打后积层清零(counter_add set——即时执行族按声明序)") var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74)) - orch0._feat_flags_for([]) - check((orch0._damage.feat_flags as Dictionary).is_empty(), - "零持有求值=零旗(relic 关着零扰动——伤害管线既有行为逐位不变)") + orch0._fx.rebuild([], [], []) + check(orch0._fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}).is_empty() + and orch0._fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": true, "terrain_kind": []}).is_empty(), + "零持有订阅=零命中(relic 关着零扰动——伤害管线既有行为逐位不变)") # 手工场景驱动:L1 全程 + L2 推进到商店房(_drive_level 复用 + 兑付确认/选路/大奖励确认手工编排) @@ -1458,10 +1542,10 @@ func _smoke_global_variant() -> void: var base_sum: int = eco._pool_weight(base_rows) check(base_sum == 77, "偏斜基线:正向池现行合计 77(挖掘风险批口径)") eco.board_skew = 0 - var off_rows: Array = eco._biased_cell_rows() + var off_rows: Array = eco._biased_cell_rows(0) # 内容数据驱动扩展批 T4:偏运权重改 reward_roll 订阅命中入参(luck_bias 注入位退役) check(off_rows.size() == base_rows.size() and _rows_weights_equal(off_rows, base_rows), "off 态零扰动:board_skew=0 行集与基线逐位相同(不进投影域)") eco.board_skew = 30 - var on_rows: Array = eco._biased_cell_rows() + var on_rows: Array = eco._biased_cell_rows(0) check(eco._pool_weight(on_rows) == 77, "合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)") var wmap := _rows_weight_map(on_rows) check(wmap.get("weaken", -1) == 8 and wmap.get("ap_up", -1) == 6 and wmap.get("gp_coin", -1) == 2, @@ -1470,16 +1554,14 @@ func _smoke_global_variant() -> void: check(wmap.get("attack_up", -1) == 15 and wmap.get("spark_aoe", -1) == 18 and wmap.get("pierce_strike", -1) == 9 and wmap.get("magic_bolt", -1) == 9, "进攻族上浮:attack 14→15 / blast 16→18 / pierce 8→9 / magic 8→9(v=30 构造例,等比上浮守恒)") eco.board_skew = 30 - eco.luck_bias = 2 - var combo_rows: Array = eco._biased_cell_rows() + var combo_rows: Array = eco._biased_cell_rows(2) # 件级加算 2=偏运硬币行 value(reward_roll bias_rewards 命中面同值) var cw := _rows_weight_map(combo_rows) check(eco._pool_weight(combo_rows) == 77 + 4 * 2, "合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8") check(cw.get("heal_minor", -1) == 5 and cw.get("gp_coin", -1) == 4 and cw.get("pierce_strike", -1) == 11 and cw.get("magic_bolt", -1) == 11, "合成序构造例:skew(heal 3)+2=5 / gp 2+2=4 / pierce 9+2=11 / magic 9+2=11(图级定型后玩家构筑叠加)") - eco.luck_bias = 0 eco.board_skew = 50 - var v50: Dictionary = _rows_weight_map(eco._biased_cell_rows()) - check(eco._pool_weight(eco._biased_cell_rows()) == 77 and int(v50.get("heal_minor", -1)) > 0, "域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)") + var v50: Dictionary = _rows_weight_map(eco._biased_cell_rows(0)) + check(eco._pool_weight(eco._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0, "域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)") # 普通图摇号全链路确定性(off 路径)——同种子普通图资源映射逐位相同(golden 对拍另有批前快照) var nrng = load("res://core/rng_service.gd").new() nrng.set_master_seed(42) diff --git a/roadmap.md b/roadmap.md index 347def52..42a455f7 100644 --- a/roadmap.md +++ b/roadmap.md @@ -64,7 +64,7 @@ | 迭代 15 | **②奇物概念收口与词族对齐批(2026-10-05 用户令编入本迭代)**:概念层已由用户逐轮裁定定稿(《奇物》v1.0→v2.7,7 轮),本批只做**落地对齐**、不做二次设计。三批内容:**①三套旧词族全面对齐**(顶层已改名「奇物」而代码仍叫旧名=用户最初抱怨的"文档与代码分开"):`feature_*`→`curio_*`(103 处/7 文件)、`relic_*`→`feat_*`(162 处/12 文件)、运行层 `bonus_*`→`curio_*`(160 处/8 文件)。**硬约束:`reward_*` 已被 `reward_pick`/`reward_node`/`reward_preview` 占用,统一词族不得使用**;`relic_table.gd` 与两个控制器类改名须同步 Godot `.uid`/`.tscn`(非文本层无法自证)。**②三条渠道按分类分离**(现行三渠道共用同一个 98 张池、三类混编):关底 Boss 击败=通过这一关→只出专长;奖励节点/商店→属性值提升+资源、专长在此**只以门票形式**出现。**兑付频率红线**:奇物不是打一场战斗给一份,关内普通战斗节点打完不给任何奇物,一关之内只兑付一次(用户 2026-10-05 纠错)。**③两个新种类入池**=刷新次数(须先与"商店独立刷新机制"划清边界)+ 专长获取门票(拿到即开不可囤积/罕见只在概率/一局限购一次/计步保底:连续 5 次三选一展示未出→第 6 次必出,刷新重展计入、奖励节点与商店共享计数);附带 `shop.tres` 补回复(默认原则要求分类内成员都在该出现的渠道池里)与 charge 标签补至 ≥4 件(现仅 3 件,档二永远达不到——数据缺陷)。设计事实源=`design/gdd/systems/冒险循环/奇物.md` v2.7,任务书=`design/research/任务书-奇物词族对齐与渠道分离批-2026-10-05.md`(T0-T8≈4.5d,两段切分预案在册〔段一词族 T0-T3≈2d/段二渠道+入表 T4-T8≈2.5d〕,切分点=T3 出口;11 条设计契约含 6 条 🔴 停机级;12 条批末验收断言;代拍板白名单 5 项;待用户裁点 4 项〔玩家可见面违规/第四关档二复裁/门票四参数终值/存档键正式迁移〕)。**开工门**:与在途 M-4 各批(判式换代/全局变异/配比预设)撞同一批文件(`adventure_table.gd`/`level_runner.gd`/`adventure.tres`),须确认不并行或用户明示插队 | 卡②门:迭代 14 出口 | 卡①已落籍(原估 0.5d);卡② ✅ **已全批落籍(2026-10-05,worktree curio-wordfamily 七提交 d4cde73..049aa62+master 同步 23547b5,主区 merge 见台账;出口报告=[iter15-2-curio-exit-report](design/qa/iter15-2-curio-exit-report-2026-10-05.md))**:三套词族代码层全换(feature_→curio_ 93 处/bonus_ 运行层→curio_/relic_→feat_——feat_table.gd/feats.tres/feats_engine.gd 改名);渠道白名单分离(总池-渠道子集多级缓存形=用户裁);门票入表(四参数 provisional 20%/2/1/5+拿到即开+计步保底+限购 1+节点商店共享计数);refresh 入池(B 案 heal 5→3 挤占)+shop.tres 补 heal 行+门票行;T2 裁点甲案=六存档键禁改(含任务书漏登记的 "relics" 键);charge 缺陷登记不硬补;autoplay 门票分支缺陷批内实测修复(MC timeout 形态);闸=冒烟 782/0(+10)+run_all 16 套历史首次全绿(reward_overlay 红转绿=M7 bcard 成因修复)+五探针绿+doc 三闸零新增+MC n200 只测不判(normal 0.5%/perfect 77.5%);遗留=07⑰ 目检归用户+C1 呈现批/C2 第四关批/C3 调参批/C4 存档批四移交。原估 T0-T8≈4.5d(实际一次会话);迭代 15 三卡全落籍(①352/②本批/③8944dca 前身③批+本批) | | 迭代 15 | **③秘宝代码词族对齐批(2026-10-05 用户令编入本迭代)**:设计侧已完成(消歧更名批 `430419c`+`06e94b7`,26 个设计文档「魔法道具」→「**秘宝**」,同调位机制名保留不变),本批只做**代码侧对齐**、不做二次设计。内容:`aux_*`/`AuxItem*` 词族全族换词 **358 处 / 36 文件**(重灾 `adventure_runner.gd`79 / `level_runner.gd`27 / `bonus_interact_controller.gd`19 / `battle_orchestrator.gd`18 / `config_db.gd`14 / `main.gd`13),分五组落:**A 玩家可见文案(P0,5 处)**=`battle_narrator.gd:117-118` + `bonus_interact_controller.gd:76` + `hero_info_panel_view.gd:56`(槽位标题 `UiKit.label`)+ `:178`(tooltip)· B schema 与数据表(`aux_item_table.gd`/`aux_items.tres`/`aux_items.gd` 文件名+类名+`ConfigDB` 持有变量与访问器全链跟改,**须同步 Godot `.uid`/`.tscn` + `--import` 重刷类名缓存**——迭代 14.5 实证在册坑)· C 文档链接修复(含 `architecture.md` 两处既有断债顺带清偿)· D 中文注释层(**须分色**:`魔法矢`/`魔法反噬`/`魔法族`/`魔法护甲` 属敌方伤害护甲语汇🔴不动)· E 测试文件名+内部变量。**用户三项拍板**:全族彻底改(含数据表)· **断点存档键一律保留**(`bonus_aux_carry`/`aux_detail`/`aux_cleared`/`aux_cleared_last`/`aux_items` 断点数组/`attunement_charges` 共 34 处引用一字不改,破档即进度丢失)· registry 条目键 `aux_items` 不改(数据契约键)。设计事实源=`design/gdd/systems/排雷循环/秘宝.md` v0.9,任务书=`design/research/任务书-秘宝代码词族对齐批-2026-10-05.md`(T0-T9≈5.0d,**超软盒 3 日上限·知情预挂**:T7 段末闸后 L3 呈报切点不擅切;**三档降级面 G1/G2/G3 在册**〔G3 下限=仅组 A 完成〕;5 条设计契约含 5 条 🔴 停机级〔存档兼容零破坏/数值零漂移/schema 契约面零变更/零字段更名/核心层零 await〕;代拍板白名单 5 项;**C1/C2 已裁关闭·全改保一致·待裁点 0 项**)。**撞批已消解**:② 奇物批 2026-10-05 收口落库(合并 `c571af2`),并行风险解除,本批为迭代 15 唯一在途需求;**T0 须重取基线**(判式换代批 T5 读数 750/0、② 又动数据层,§1.2 的 747 为②前旧数禁沿用)并复核事实表。**✅ 裁决(用户 2026-10-05「全改保一致」)**:C1 `"aux_item"` 域串**照改**(实测连锁面收窄——`reward.tres` 已零 aux_item 数据行,池种随迭代 11 T7 退役;16 处字面量全在代码内闭合无存档依赖;唯一连锁项=`smoke_all.gd:3932` 断言须同步)· C2 `aux_items` 一名两义**拆分**(ConfigDB 持有变量+表注册键✅改 / 断点存档键+冒烟夹具🔴禁改——**「键名禁改」与「变量名禁改」是两回事**,禁改的只是落盘字面量)。新增**组 F**(分派语汇与域串 16 处),任务卡扩为 T0-T10≈5.5d。**开工门**:✅ 已无待裁点,可直接发 T0 | 卡③门:迭代 14 出口(**裁点已全决·可开工**) | 卡①已落籍(原估 0.5d);卡② ✅ 已全批落籍(2026-10-05 merge 3c6f2c9,台账 362——c571af2 文档+数据层之上的实施段全量补完);**卡③ ✅ 已全批落籍(2026-10-05,worktree m4iter15-mibao 九提交 b4b8bb3..收口笔,主区 merge --no-ff 哈希见台账/active.md 快照行;出口报告=[iter15-3-mibao-exit-report](design/qa/iter15-3-mibao-exit-report-2026-10-05.md))**:词族实数 364 处/36 文件全换尽(例外 43+2 处=存档键引号字面量+夹具,穷举在报告 §五);闸=冒烟 755/0+run_all 14/15(唯一红=②批遗留基线红 reward_overlay,对照零新增)+行为探针四件全绿+doc 三闸/熵增闸零新增+存档键六键普查零差异+MC 按 goal 令豁免(静态零漂移证明代);渲染像素五闸屏蔽挂账 07⑮;遗留=实机目检归用户(07⑮)+reward_overlay 基线红归②批处置 | | 迭代 16 | **UI 设计对齐批(2026-10-05 用户令:迭代 16 改编为 UI 优化需求实现,原四卡全数顺延迭代 17;美术线用户自理立规的例外面=本批只改 UI 设计代码、零美术资源改动)**:三路改进整合轴的代码侧落地——事实源=`design/research/UI预览demo三路改进整合对照-2026-10-04.md`(A 落码差异对照+OPT-A 机检+WB 变体三路整合,基线原型 v2.1)。**红线=不修改美术资源**:game/assets/art 与 artwork/ 均不动、不生新素材不重烘,只改 UI 设计代码(ui_kit 色板 token/字号档/布局纪律/对比度垫底/抖动呈现/panel_skinned 接线等)。无分歧可直接收项=对比度 7 处 FAIL 修法(血数 2.09→12.29 垫底等已复算)/站桩溢出 3.2px/4 处裸色收 ui_kit/status 图标 preload 违规/tg-* 类型色接线。**前置待裁=六分歧点**(①底色冷暖 ②棋盘格边长 32/44/45 ③字号档位 ④九宫格切片 ⑤圆角去留 ⑥图标尺寸解法,对照表 §三)+开放项(抖动范围/色板唯一来源,§五)。任务书=`design/research/任务书-UI设计对齐批-2026-10-05.md`(T0-T11≈7.25d,**超软盒 3d 预挂**,三段切分=段一 2.0d 无阻断〔T0-T4a〕/ 段二 4.0d 待裁〔T4b-T8〕/ 段三 1.0d 收口〔T9-T11〕;🔴 契约 7 条 R1-R7;无分歧收项 N1-N6 已逐处 file:line 考证;**待用户裁点 11 项**=六分歧 P1 底色冷暖/P2 棋盘格边长/P3 字号档位/P4 九宫格切片/P5 圆角去留/P6 图标尺寸解法 + 开放项 P7 色板唯一来源/P8 抖动范围/P9 §五 已决注销清单/P10 记忆态口径/P11 装饰 caption 豁免;**六分歧与开放项一律呈全备选案含淘汰依据**)。**事实校正(实查,2026-10-05)**:9 处色板漂移实测 4 已收 5 未收;`tg-*` 五族在 ui_kit 并不存在(以裸 Color8 内联于 bonus_pick_overlay/shop_overlay_view,缺第五族);panel_skinned 消费面实测 8 处+工厂 1 处;棋盘格边长=动态公式 clamp[26,46] 与 §8.1 锚点 45/36 三方互不等;手绘图标十枚 20×20 已入库,真问题转为 64×64 柜台四枚显示倍率;站桩溢出实机存在 1px 余量同型写法(standee_body.gd:476);demo 基线 md5 已再次漂移(实测 78ebbee0…)。状态=**任务书已起草(2026-10-05)+ 取向已定(2026-10-06)**:✅ P1=甲(维持暖化零改动)/ P2=丁(棋盘格边长零改动,46/45/44 不一致登记为已知偏差);✅ **其余八项取向已定**(用户采纳推荐)——**跟 demo**:P4 九宫格切片 8→16 / P7 五处色板向 demo / P10 记忆态纹理灰罩;**按条款解**:P3 字号立 FS_* 档(demo 34 处<14、54 处<16 不可跟)/ P6 图标显示档(demo 自身 0.3125× 违规)/ P8 抖动只做卡顶色带(demo 渐变+blur 违反像素纪律)/ P11 装饰 caption 12px 豁免;P5 两路同结论。**「以 demo 为准」三层优先级栈=任务书 §8.3**(①用户终裁>②冻结条款>③demo);三条结构级差异(卡片 232×352 / 商店全屏化 / 地图底栏合并)**不入本批、已登记 06 待立项**。估时 T0-T11≈**6.9d**(段一 2.15d 无阻断可发令 / 段二 3.75d / 段三 1.0d;三段同属一迭代、**只在段三末合并一次**);**拷打轮一 9 问已全部落地**(Q1 `panel_skinned` content margin clamp / Q2 不走独立样张闸改并进 T10 目检 / Q3 柜台图标统一 32px / Q4 商店补类型分族 / Q5 血数 11px 与装饰 caption 合并写一条豁免 / Q6 槽两态不新增通道 / Q7 一次合并 / Q8 已知偏差已登记 06 / Q9 demo 取值抄录表为唯一引用源)。**✅ 已全批落籍(2026-10-06 单会话,worktree g-plan2-iter16ui 分支 iter16-ui-20261005 十二提交 85ea024..T11 收口笔,主区 merge --no-ff 哈希见 active.md 快照行;出口报告=[iter16-ui-exit-report](design/qa/iter16-ui-exit-report-2026-10-06.md))**:T0-T10 全落(T10 人闸材料包=[iter16-ui-human-gate-checklist](design/qa/iter16-ui-human-gate-checklist-2026-10-06.md)——实机目检+代拍板追认八项归用户);闸=冒烟 782/0(断言数持平基线)+run_all 16 套全绿+探针五件全绿(新增 ui_contrast_probe 11 对测点+ui_struct⑥standee 断言族入默认段)+doc 三闸/熵增零新增+MC 零跑(纯呈现批——核心/数据层 diff=0 实证)+素材零触碰;T0 实机复算三处修法改向(意图徽标垫牌/槽位字深化/RAP 零改动——demo 口径在实机反向)+N4a 裸色实数 25 处/10 文件全收 ui_kit(任务书 5 处系漏记)+字号 107 位点归档 FS_* 六档;偏差八条在出口报告§汇总;遗留=06 新登记「呈现层残余 <4.5:1 文字位」一条(名行四墨/顺位徽标/RAP 反馈——批外邻接发现) | 迭代 15 出口 | 预算 T0-T11≈6.9d(三段切分)——**实际单会话一次跑完(2026-10-06)** | -| 迭代 17 | **⓪内容数据驱动扩展批(2026-10-06 用户令插队为头卡——①-④卡顺延,其任务书与裁点状态原样保留)**:把内容效果行为从结算代码硬编码搬进数据行「订阅事件(on)+条件(when)+效果类型(do)」,通用解释器 EffectEngine 统一执行——完成后加「已有效果类型」的新专长/秘宝=纯改 `.tres` 零代码。裁决八条全落(R1 发布订阅同步 emit 确定性优先/R2 仅一个改写窗口不做拦截器链/R3 一事件多订阅+组合规则封冲突面/R4 旧 23 挂点枚举删除·2 个退化条件行/R5 现有实现全改造+06 在册只登记〔curio_roll/shop_purchase/gp_settle 三事件登记不实施〕/R6 框架先行发射点不论有无订阅者全埋/R7 一个迭代四模块垂直切一次合并/R8 术语纪律:事件=发布订阅 topic、效果类型=订阅后逻辑、组合规则=叠加规则,禁 hook/合并律等自造词)。词汇表定稿=**实施 16 事件**+效果类型 19 行 23(v1.1 扩 damage_mult/open_ticket 两型装下减半/开单现状)+组合规则 6;存档零改动;文案表值化随批(现有实现判据);luck_coin 经 reward_roll 迁移随批。任务书=`design/research/任务书-内容数据驱动扩展批-2026-10-06.md`(v1.1 2026-10-06 联审修订:level_clear 归登记不实施/对拍判据两套〔确定性复放与冒烟=逐位、MC 统计量=成型画像同带 m4iter12 口径〕/映射表五处语义修正/断言面随迁与订阅重建时机补列;T0-T7≈4.5d,零待裁点,🔴 契约 5 条含行为等价两套判据+护栏保留+确定性;停机点 S1 首流派对拍/S2 批末闸;开工门=U0) | 迭代 15 出口(已解) | T0-T7≈4.5d | +| 迭代 17 | **⓪内容数据驱动扩展批(2026-10-06 用户令插队为头卡——①-④卡顺延,其任务书与裁点状态原样保留)**:把内容效果行为从结算代码硬编码搬进数据行「订阅事件(on)+条件(when)+效果类型(do)」,通用解释器 EffectEngine 统一执行——完成后加「已有效果类型」的新专长/秘宝=纯改 `.tres` 零代码。裁决八条全落(R1 发布订阅同步 emit 确定性优先/R2 仅一个改写窗口不做拦截器链/R3 一事件多订阅+组合规则封冲突面/R4 旧 23 挂点枚举删除·2 个退化条件行/R5 现有实现全改造+06 在册只登记〔curio_roll/shop_purchase/gp_settle 三事件登记不实施〕/R6 框架先行发射点不论有无订阅者全埋/R7 一个迭代四模块垂直切一次合并/R8 术语纪律:事件=发布订阅 topic、效果类型=订阅后逻辑、组合规则=叠加规则,禁 hook/合并律等自造词)。词汇表定稿=**实施 16 事件**+效果类型 19 行 23(v1.1 扩 damage_mult/open_ticket 两型装下减半/开单现状)+组合规则 6;存档零改动;文案表值化随批(现有实现判据);luck_coin 经 reward_roll 迁移随批。任务书=`design/research/任务书-内容数据驱动扩展批-2026-10-06.md`(v1.1 2026-10-06 联审修订:level_clear 归登记不实施/对拍判据两套〔确定性复放与冒烟=逐位、MC 统计量=成型画像同带 m4iter12 口径〕/映射表五处语义修正/断言面随迁与订阅重建时机补列;T0-T7≈4.5d,零待裁点,🔴 契约 5 条含行为等价两套判据+护栏保留+确定性;停机点 S1 首流派对拍/S2 批末闸;开工门=U0) | 迭代 15 出口(已解) | T0-T7≈4.5d |**状态**:**已实施完成(2026-10-06,worktree iter17-dd 待合并)**——T1 效果引擎框架+16 发射点(7d157b3)/T2 词汇落码(225765f)/T3 专长 27 行迁移+旧挂点消亡(e28a6cb)/T4+T5 秘宝六件+令牌树收尾(a33ddaf);六闸全绿(冒烟 800 项 0 失败/单元 17 套/探针含 click/MC 二档对拍 40 局同种子逐位一致);出口报告 design/qa/iter17-datadrive-exit-report-2026-10-06.md,遗留人闸两项随报告 | | 迭代 17 | **①敌人表行集多行批(2026-10-05 用户令编入本迭代,纯功能与数值轴四卡之一)**:放开敌人表「同一(关卡序号 × 节点深度)行集恰一敌种」加载期校验(enemy_table.gd:108-112 考证在册),允许同场遭遇混编多种敌人——生成(unit_container spawn 行集受理)/编排(battle_orchestrator/narrator)/断言面全链接通 + GDD 结构冻结面修订先行(敌人系统.md:75/:263)。事实源=06「同深度第二敌种与精英/Boss 编码位」条剩余面(前半已由关卡节点地图 v1.0 消解);前置=技能池实施批已落籍(d3fa887)开工门已解。任务书=`design/research/任务书-敌人表行集多行批-2026-10-05.md`(**v1.1 2026-10-06 联审修订**:F7 勘误 19 行 9 敌种/P1 补遭遇骰交互〔每行一骰·RNG 扩签先报批〕与实例总数≤4 消歧/P3 改裁丙撤投放/新增契约 R6 混编行行序纪律/expect_pools 断言换代入组 D;T0-T8≈3.75d〔T5 撤投后,超软盒预挂,T4 末切分点〕;🔴 契约 6 条;**✅ 裁点全裁(2026-10-06 用户令:P1甲〔Q8/Q9 二裁补全〕/P2甲/P3丙〔Q3 二裁撤投放——一裁乙与首遇教学位纪律三面冲突〕/P4乙 Boss 行亦放开·知情代价六面在册);开工=U0**) | 迭代 15 出口 | T0-T8≈3.75d(超软盒预挂,T4 末切分点) | | 迭代 17 | **③暂停层只读入口批(2026-10-05 用户令编入本迭代)**:新增局内暂停层,承载钥石余额与解锁态(营地三柜台进度)只读查看——事实源=06「解锁界面与钥石呈现的交互形态(残余:暂停层只读入口)」条。考证在册:现行无暂停层;模态栈/设置面/调试浮窗三先例齐;UiKit 公共件(modal_shell/bcard/verb_button/徽章形制)与色板 token 单点现成;MetaProgression 只读访问器面齐全;战斗屏 GP/钥石已迁出=局内零查看入口正是本批由来。**美术红线:复用现行公共件与色板,零新素材零新色值(2026-10-05 用户立规)**。任务书=`design/research/任务书-暂停层只读入口批-2026-10-05.md`(T0-T5≈3.0d;🔴 契约 5 条含公共件单点/模态登记纪律;**✅ 裁点全裁(2026-10-06 用户令全按推荐:P1丁/P2甲/P3甲/P4甲);开工=U0**) | 迭代 15 出口 | **✅ 已全批落码待人闸(2026-10-06,worktree g-plan2-iter17-pause 分支 iter17-pause-20261006 两会话接力完成,提交 c08a901/fc9eb1e/d44fdd5+收口笔;出口报告=[iter17-pause-exit-report](design/qa/iter17-pause-exit-report-2026-10-06.md))**:暂停层本体+badge 工厂件收编(Q10 甲,营地钥石徽同步迁移观感零变化)+两屏常驻角标入口与 has_open 守卫+四探针断言(结构/点击/输入锁 case⑯/只读巡检/零动效契约/对比度对)+冒烟投影一致性 5 断言;闸=冒烟 788/0(基线 783/0)+run_all 17 套+probe_gate --click 全绿+doc 三闸零新增+MC 零跑(纯呈现零数值);存量移交=熵增闸 design/diagrams 越界(00712d5 总图批落籍漏步,主区同红)+06/07 gdd_lint 存量注记红(本批零触碰);**人闸=用户实机目检**(design/qa/iter17-pause-human-gate-checklist-2026-10-06.md,重点含排播排空副作用确认项 D2。原估 T0-T5≈3.0d 贴软盒内) | | 迭代 17 | **④生涯界面设计立项(2026-10-05 用户令编入本迭代;只做设计提纲不做实装)**:事实源=06「『生涯』界面——冒险中数据回看的唯一承接处」条(P-C7 裁:M9 已退役局内结算明细回看,历史数据现无承接面=用户知情裁决)。考证在册:回看退役已落码(battle_end_overlay/battle_log_view/ui_assembler 三处注记)+settlement_review_overlay.gd 遗留件在盘+跨局持久面=save v5 meta 域+recent_summaries 现役逐局留痕(cap=10,save_manager.gd SUMMARY_CAP——2026-10-06 联审勘误:原记「无历史冒险记录面」系事实错误;真实缺口=cap 扩容+用时埋点+总局数/通关局数 lifetime 计数域〔涉存档 schema〕+营地既有最近冒险清单的去留设计)。任务书形态=设计提纲版:信息架构+承载内容清单(跨局汇总/历史冒险回看/图鉴承接)+与既有界面关系+三期实装建议+待裁点,标注「实装批另行立项」。任务书=`design/research/任务书-生涯界面设计立项-2026-10-05.md`(**v1.1 2026-10-06 联审修订**:F4 改正 recent_summaries 现役/P2 甲口径重述=扩容+用时埋点/总局数通关局数标注涉 schema/营地清单去列入设计稿必答项/N 候选补「维持 10」;T0-T2≈1.25d 纯文档零测试;**✅ 裁点全裁(2026-10-06 用户令全按推荐:P0甲/P1甲/P2甲〔N 实装期定〕/P3甲/P4甲/P5甲);开工=U0**) | 迭代 15 出口 | ✅ **设计提纲已成文待用户评审(2026-10-06 卡④设计批完成,worktree iter17-career-20261006,纯文档零代码零测试)**:提纲=design/gdd/systems/跨局循环/生涯.md v0.1(范围草案——信息架构/承载内容清单/界面关系/分期建议/待裁点五节,P0-P5 已裁结论落文,新发现待裁点 P6 营地最近冒险条带去留随评审一并裁);02 根索引补行+06 状态行换代「设计提纲已成文待评审」。**人闸=用户评审提纲**;评审通过后期一/二/三实装批另行立项(不预占排期。原估 T0-T2≈1.25d 软盒内) |