merge: 迭代 20 专长实装批全量入库(T0-T10——词表扩签/案D池/29行换代/凭证五件/Boss加权/MC基线/迁出收尾/空配置闸;批末重闸全绿冒烟864/0+run_all21套;实现报告=design/qa/iter20-实现报告-2026-10-11.md)
This commit is contained in:
commit
34c1564614
28 files changed
+2482
-1071
No files matched your search
@@ -144,7 +144,8 @@
|
||||
|
||||
**状态**:已编入迭代 22(2026-10-11 用户令;需求卡片 v1.2 零待裁——开工门=U0)
|
||||
|
||||
**进展注记(2026-10-10)**:数据文件面本批不依赖锻造工坊线;**槽位面依赖修正(2026-10-10 设计骨架批后)**——同调位数量与品质的语义已裁归天赋树同调位节点(树侧接线归天赋树线/天赋树实装批),本批与该批存在槽位定序点(先树后宝或合并评估,随排期裁),现役谓词表求值作为本批落地前的过渡基座仍成立;秘宝体系文档正确性检查已过(五文件交叉核对:仅 registry 三处现行旧称「魔法道具」残留,已随检查批修正;其余命中均为版本历史与更名记载的合法引述)——**检查通过;设计侧裁点经 grilling 审查轮+设计骨架批已清零(秘宝.md v2.3),按三段流程设计段完成**。**③专长侧联动已拆出独立成卡(2026-10-11 用户发令;同日用户纠正=奇物线单独成卡、秘宝面不并入)**:design/research/需求卡片-专长实装批-2026-10-11.md(T0-T10 ≈10-12d 单迭代交付;**Q1-Q3 已全裁清零**〔Q1 Boss 兑付换代/Q2 占位值/Q3 单迭代〕——**卡片成立,零待裁点,状态=已编入迭代 20(2026-10-11 用户令,待开工 U0)**;奇物侧设计+命名+裁定齐备〔奇物.md v3.15〕)——本条目余项(①②④⑤⑥⑦)维持冻结待发令,其中⑥效果引擎扩签核实与停手挂点由专长实装批 T1 先行承载。两条线交叉核对点一处随本批 T0:feats.tres 的 magic_vs_magic 条件值所属专长件是否在素材库表二 27 件删除名单内(若在,锻造工坊实装批③「改值复活」随之作废)——**已核结(2026-10-11 锻造工坊裁决会话 D 组)**:所属件=魔网织者(mana_weaver),在名单内,③「改值复活」作废,废卡随专长 27 件退役删行批消解。
|
||||
**进展注记(2026-10-10)**:数据文件面本批不依赖锻造工坊线;**槽位面依赖修正(2026-10-10 设计骨架批后)**——同调位数量与品质的语义已裁归天赋树同调位节点(树侧接线归天赋树线/天赋树实装批),本批与该批存在槽位定序点(先树后宝或合并评估,随排期裁),现役谓词表求值作为本批落地前的过渡基座仍成立;秘宝体系文档正确性检查已过(五文件交叉核对:仅 registry 三处现行旧称「魔法道具」残留,已随检查批修正;其余命中均为版本历史与更名记载的合法引述)——**检查通过;设计侧裁点经 grilling 审查轮+设计骨架批已清零(秘宝.md v2.3),按三段流程设计段完成**。**③专长侧联动已拆出独立成卡(2026-10-11 用户发令;同日用户纠正=奇物线单独成卡、秘宝面不并入)**:design/research/需求卡片-专长实装批-2026-10-11.md(T0-T10 ≈10-12d 单迭代交付;**Q1-Q3 已全裁清零**〔Q1 Boss 兑付换代/Q2 占位值/Q3 单迭代〕——**卡片成立,零待裁点;✅ 专长实装批已入库(2026-10-11 迭代 20 T0-T10 全落 master——③专长侧联动由该批承载销账:feats.tres 29 行换代/词表扩签/停手挂点 dig_stop 全落)**;奇物侧设计+命名+裁定齐备〔奇物.md v3.15〕)——本条目余项(①②④⑤⑥⑦)维持冻结待发令,其中⑥效果引擎扩签核实与停手挂点由专长实装批 T1 先行承载。两条线交叉核对点一处随本批 T0:feats.tres 的 magic_vs_magic 条件值所属专长件是否在素材库表二 27 件删除名单内(若在,锻造工坊实装批③「改值复活」随之作废)——**已核结(2026-10-11 锻造工坊裁决会话 D 组)**:所属件=魔网织者(mana_weaver),在名单内,③「改值复活」作废,废卡随专长 27 件退役删行批消解。
|
||||
|
||||
|
||||
#### 效果引擎条件事实扩签——生命比例(背水实现前置需求)(2026-10-11 用户批登记)
|
||||
|
||||
@@ -152,7 +153,7 @@
|
||||
|
||||
**归属**:[奇物](systems/冒险循环/奇物.md)「扩签登记」节(词表扩项现行事实源)+ 效果引擎(`game/core/effect_engine.gd` 条件事实词表修订随实装批)
|
||||
|
||||
**状态**:待办(冻结——迭代规划冻结覆盖,随专长实装批重启)
|
||||
**状态**:✅ 已落地销号(2026-10-11 专长实装批 T1——hp_ratio 事实入 FACTS 词表〔百分比 int 口径〕,背水行 when hp_ratio lte 50 在役;effect_words_t1_test 钉面)
|
||||
|
||||
#### 内部分类禁写入玩家可见面——奇物呈现三处违规待改(2026-10-04 用户立意登记)
|
||||
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
|
||||
## 行为变化速览(体验前先读)
|
||||
|
||||
### 专长实装批追加(2026-10-11 迭代 20——实机目检项,登记面)
|
||||
|
||||
- **Boss 专长三选一呈现**:非末关通关停点候选=全专长新 29 件(按档配比加权——普通打底/稀有稀缺);旧「提升三选一」属性件不再出现于通关渠道;回血读数照旧随载荷。
|
||||
- **曝光槽三卡观感**:奖励节点/商店三选一=2 格全池+1 格优秀档以上子池(子池空回退全池);新凭证四件(储备金袋/同调电池/时沙/完美保险)稀有档投放观感。
|
||||
- **卡面文案与稀有度档位显示**:新 29 件专长 display_name/desc 直读行字段(封印/陷阱风味系命名);四级档位(普通/优秀/稀有/传说)在卡面档位显示位是否正确渲染。
|
||||
- **同调电池选件面**:拾取同调电池后的充能分配交互(choose_battery_target 核心面已落,呈现层选件窗=本批登记观测——若呈现缺窗,登记缺陷回反哺点)。
|
||||
- **HUD 特性段降级态**:流派聚合行退役后 C 区逐件行显示是否可读(呈现改法归 06 在册,本批不随批改)。
|
||||
|
||||
|
||||
| 改动 | 旧行为 | 新行为 | 直接体感 |
|
||||
|---|---|---|---|
|
||||
| 硬化(敌方技) | 主角打硬化目标**翻倍**(符号 bug) | 主角攻击**折半** | Boss 战显著变长变难(预期内) |
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# 迭代 20 实现报告——专长实装批(奇物)
|
||||
|
||||
> 2026-10-11 goal 会话(iter 技能 U0 开工)——需求卡片 v1.1 T0-T10 单迭代全量交付;A/B/C 三段推进。
|
||||
> 定义面:design/research/需求卡片-专长实装批-2026-10-11.md(v1.1,Q1-Q3 已裁零待裁)。
|
||||
|
||||
## 一、各卡状态
|
||||
|
||||
| 卡 | 内容 | 状态 | 提交 |
|
||||
|---|---|---|---|
|
||||
| T0 | 开工三查+状态账核实+magic_vs_magic 交叉核对+解耦三案选型 | ✅ | (结论落本报告 §三/§四;开工记录 active.md 462) |
|
||||
| T1 | 效果引擎词表扩签+发射点施加面 | ✅ | a57489e |
|
||||
| T2 | 池结构案D(四级档位/令牌+cap 解耦案②/曝光槽 2+1) | ✅ | 0640b0c |
|
||||
| T3 | 专长数据行换代(feats.tres 29 行)+流派框架退役 | ✅ | 54ab330 |
|
||||
| T4 | 凭证件五件+留任二件+F-1 刷新写错账修复 | ✅ | 1387aed |
|
||||
| T5 | Boss 兑付面换代(按档配比加权抽取+正名) | ✅ | 3333187 |
|
||||
| T6 | 件级跨事件状态账(adventure 域计数器) | ✅ | a57489e(承载)/5ebd05d(V4 断言) |
|
||||
| T7 | 断点存档扩容 | ✅ | 1387aed(扩列)/3333187(V16 断言) |
|
||||
| T8 | MC 对账适配+基线重投 | ✅ | 3333187 |
|
||||
| T9 | 迁出收尾(treasure_items 两行+词面+rewrite_board 除名) | ✅ | 3333187/710d8e0 |
|
||||
| T10 | 空配置验收闸+文档同步闸 | ✅ | 3333187/8fd9909 |
|
||||
|
||||
## 二、验收清单核销(V1-V23)
|
||||
|
||||
- **V1 词表封闭**:effect_words_t1_test 17 checks(新词在册/出域 on-fact-op-kind 四路被拒/adventure 计数器 export-seed 往返/mod_eq 预评估口径/V4 计数三态)。运行期 dispatch fail-fast=debug assert(release 剥离系引擎语义,自动化覆盖走加载期 validate 致命通道——effect_engine.gd dispatch 首行)。
|
||||
- **V2 全量替换**:29 行落表(feats validate 零违例)、旧 27 件 row_by_id 空、牌库 104+29=133 四级断言、全仓 grep 零残留(4 处 source_class 中性名换代 710d8e0;对照性注释留痕豁免)。
|
||||
- **V3 逐件一致**:附表 A 29 件三列断言入 smoke 特性段(命中面 dispatch 直测+counter 落账施加面同构 helper;正常/边界/反例三列逐件在案)。
|
||||
- **V4 状态账**:跨事件累积/counter_spend 消耗/reset_at 声明制两态(effect_words_t1_test)。
|
||||
- **V5 四级配额锚**:TIER_QUOTAS 70/25/9/0=104 硬断言+smoke 每档锚+deck_total(V14 三档锚摘除换代)。
|
||||
- **V6 令牌三态 / V7 曝光槽 / V8 解耦**:pool_case_d_test 17 checks(拿了消耗/没拿回池/只刷出不计;2+1 回退/凭证不进子池/池空兜底;cap 独立/提前退役只剔候选视图)。
|
||||
- **V9 凭证五件路由 / V10 留任+AP 属性轴**:feat_vouchers_test 23 checks(逐件路由+F-1 反例钉面+电池禁自动替选+留任路由零变化+bonus.ap 落点)。
|
||||
- **V11 Boss 兑付**:通关渠道全 relic(白名单断言在案)+回血载荷在役+末关不发(既有 adventure smoke)+信号注释换代「专长三选一」。
|
||||
- **V12 档配比加权**:pool_case_d V12 频带断言(300 停点×3 候选,权重 100/45/15/5×件数 12/9/8/0→期望 69/26/4/0,带 ±15pp)。
|
||||
- **V13 迁出零残留**:treasure_items 无两行+同调目录收窄断言+全仓 grep 收口(中性名换代)。
|
||||
- **V14 旧验证约束同生死**:smoke 三档锚/数值对/池构成/上限带/FeatsEngine 流派段/深挖钻头回归段/七流派订阅面全段摘除或换代(主语换代新 29 件);battle_orchestrator FeatsEngine 求值注入摘除。
|
||||
- **V15 MC 新基线**:normal/off 100 局 seed 71000 档=通关 0%、止步 {1:93, 2:7}(master 对照 0%、{1:90, 2:10}——零配基线本相,分布差可归因曝光槽候选构成);同种子 30 局重放逐位复现 ✓。
|
||||
- **V16 断点扩容**:adventure_counters/refresh_stock/battery_pending 入快照+续玩回填逐项一致(feat_vouchers_test)。
|
||||
- **V17 空配置闸**:池空 skip_empty_curio 消化+非空拒绝反例+商店空货架合法态(pool_case_d)。
|
||||
- **V18 层级纪律**:牌库 kind 全集=CURIO_KINDS+relic 断言;同调目录零 feats 行(treasure_items 两行迁出收窄)。
|
||||
- **V19 棋盘事实不可变**:29 行零棋盘改写类订阅断言(rewrite_board 除名);棋盘生成不看持有集(生成面零接触)。
|
||||
- **V20 排雷零特判(人工核对)**:静态 grep 排雷侧(minesweeper_rules/board 生成)零按件名/feat_id 分支;fx_reveal=通用效果通道(安全格校验+fx_reveal 标签,非件特判);效果接入全部经订阅面。**待用户复核**。
|
||||
- **V21 命名纪律(人工核对)**:机检半=29 行 display_name/desc 零「雷/插旗」断言在案;**人工过目半待用户**(07 目检项)。
|
||||
- **V22 数值纪律(人工核对)**:机检半=陷阱爆发 mult>1/血蜜回复<典型陷阱伤/石肤 cap 3 断言在案;全表 provisional 占位值对照奇物.md 结构锚逐项过目**待用户**。
|
||||
- **V23 配额重裁纪律(人工核对)**:四级锚数字与裁决记录一致性**待用户**(见 §三代拍板追认单 3)。
|
||||
|
||||
## 三、代拍板追认单(白名单授权内,逐项待用户追认)
|
||||
|
||||
1. **件数口径 29 行**(卡文 §10.4 推荐案执行):基础 27+迁入 2=feats.tres 29 行;V2/V3 断言按落表实数。
|
||||
2. **四级配额 70/25/9/0=104**:T2 直映射 98(零偏离已裁三档数字)+T4 凭证四件入稀有档投放 6(rap 2/attune_battery 1/time_sand 2/perfect_insurance 1,provisional 归调参批)。偏离 GDD 占位=呈报记录即本条(GDD 无凭证投放占位数)。
|
||||
3. **传说档张数 0**:头奖位留空(奇物.md「传说=惊喜位」形态内取空集——零内容件可投)。
|
||||
4. **词表落法映射**:FACTS 实扩 9 项(hp_ratio 百分比 int 口径/pct 等——奇物.md 扩签登记 10 候选中「本轮挖格花费/本轮开格数/本轮踩雷数计数器/类型集合计数器」落 counter+value_event 不占 fact 词位);新事件 2(dig_stop/battle_won——battle_won 系战地包扎声明所需,扩签登记未列,落法映射补位);新效果类型 7+算子 mod_eq(mod_eq value=模数 ref=余数——双管镐每第 2 次预评估口径)。
|
||||
5. **「积满触发」预评估落法**:dispatch when 于命中前读计数器现值——行 when 按 N-1 落值(地质学家 eq 3/跳格 eq 2/封印匠 mod_eq ref 2/保险丝 eq 1);单元测试钉住。
|
||||
6. **疼痛记忆消费时机放宽**:设计「下场战斗攻击附加层值」——实现为「下一次攻击」(同战内当轮可消费;跨战持久经 adventure 域账本)。同战内不可用判定无法行内表达,放宽口径随本条呈报;若用户否,补 battle-gate 面(效果引擎±1 键)。
|
||||
7. **时沙落法**:「下轮 AP +2」落下一战首轮 AP 队列(hero_ap_bonus 同通道);若拾取时战斗在途(奖励节点停点)则下一战首轮兑付。
|
||||
8. **弃图兑换落法**:「折为下轮伤害」落停手后首次攻击全额附加(用后清零)——同战停手→战斗段序即「下轮」前移;多攻击不拆分。
|
||||
9. **血税落法**:bonus_delta 战末汇入冒险加算区(本战内 max_hp 不即时变,下一战生效——与效果引擎重建时机契约一致)。
|
||||
10. **潜伏缺陷修正两件**(T2/T4 随批):①效果引擎 has 算子 null 安全(敌技伤害载荷无 terrain_kind 事实时判「不在场」而非 String(null) 抛错——候选流变化后 devour 件首次被持有暴露);②刷新券旧兑付早退零摘牌(令牌永不消耗)——选中即消尽修正。
|
||||
|
||||
## 四、GDD 反哺点清单(设计权威文档不随批,逐点待用户裁决后另批落文)
|
||||
|
||||
1. **奇物.md**:①「每第 N 次/积满触发」的 when 预评估口径(N-1 落值)宜入设计原则注记;②疼痛记忆消费时机实现放宽(§三.6)如追认宜回写件表;③血税/时沙/弃图兑换三件落法映射(§三.7-9)如追认宜回写件表「账」列;④rewrite_board 效果类型除名(净化工兵退役连带)——扩签登记「暂缓:清图事件」条相应更新。
|
||||
2. **冒险结构.md**:「关卡大奖励」节陈述(通关两层兑付→回血+专长三选一)——实现已为该形态且叠加按档配比加权抽取(奇物.md v3.1 语义约束兑付面),宜对齐表述。
|
||||
3. **难度曲线与Boss.md**:配额投放读数(四级 104 构成/曝光槽加速高档获取——止步分布 90/10→93/7 位移归因记录)宜随数值批复核。
|
||||
4. **02-系统清单.md**:奇物行状态(专长 29 件已实装/流派机制退役)宜换代。
|
||||
5. **奇物呈现**:HUD 特性段流派聚合行退役→逐件行降级态在役;呈现改法归 06 在册「奇物呈现三处违规改法与档位呈现三路」条(本批零随批改动)。
|
||||
|
||||
## 五、实机目检总清单(人闸——07-体验验证清单同面登记)
|
||||
|
||||
1. Boss 专长三选一呈现(候选全专长/按档配比观感/回血读数)。
|
||||
2. 曝光槽三卡观感(2+1/子池回退/新凭证四件稀有档投放)。
|
||||
3. 卡面文案与稀有度档位显示(29 件 desc 直读/四级档位渲染)。
|
||||
4. 同调电池选件面(choose_battery_target 呈现窗是否缺位——缺位登记缺陷归反哺)。
|
||||
5. HUD 特性段降级态可读性。
|
||||
|
||||
## 六、批末闸读数(证据只留最新轮)
|
||||
|
||||
- 冒烟 **864/0**(基线 846+换代净增 18);run_all **21 套件全绿**(新增 effect_words_t1/pool_case_d/feat_vouchers 三套);probe_gate --click/--full 全绿残留 0;render_gate 全绿(golden 3 屏 0.00%);doc 三闸对基线零新增(worktree 2 死链均系 worktree 无未跟踪文件伪影:filter-repo 计划书系 .zcode 未跟踪件/config_fatal_probe 两树共有);熵增闸 PASS;MC 同种子重放逐位复现 ✓。
|
||||
- 新基线读数:normal/off 100 局 seed0=71000——通关率 0%(master 对照同 0%)、止步分布 {1:93, 2:7};form 画像=五事件族 14 件(adventure_monte_carlo.gd 换代)。
|
||||
@@ -603,6 +603,7 @@ entities:
|
||||
boundary: 判据走生命周期——**一次冒险内获得、冒险收束即清零的是奇物;跨冒险跟着你走的是秘宝**。奇物永不进同调位,秘宝永不进当局随机渠道;与元进度天赋树两层互不代位
|
||||
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 / 算子封闭;四词表出域=加载期校验致命);组合规则六族(加法/乘法/任意真/按序改写/按序消耗/即时执行)为效果类型必填属性;遍历序=数据表声明序写死保种子复放;订阅重建=每战开始从持有集/装备集重建(中途获得下一战生效)
|
||||
iter20_rebuild: **专长重设计换代(2026-10-11 专长实装批迭代 20)**——feats.tres 29 行(基础 27+迁入淬毒刃纹/封印爆裂,卡文 §10.4 口径)整表替换旧 27 件〔素材库表二〕;rarity 四级(普通 12/优秀 9/稀有 8/传说 0)直映射令牌档+cap 已拿上限字段;**流派标签/档位激活机制退役**(奇物.md v1「不设流派标签不设档位」——ARCHETYPES/FeatsEngine 档位质变摘除,charge 档二不可达缺陷随框架退役消解);词表扩签=事件 18(+dig_stop/battle_won)/效果类型 30(+heal/ac_bonus/damage_reduce/bonus_delta/blast_scaled/preview_area/target_attack_bonus,rewrite_board 除名)/事实 18(+生命比例等 9)/算子 +mod_eq;池结构案D=四级档位 70/25/9/0=104+曝光槽 2+1+概率上限解耦(案②令牌池+提前退役 cap)+Boss 按档配比加权抽取;凭证五件入池(刷新券 F-1 修复/储备金袋/同调电池/时沙/完美保险);adventure 域计数器跨战账本(domain 键+counters_export/seed)
|
||||
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 加算区四轴不含它)。两者不是一回事,禁止混算。
|
||||
@@ -624,6 +625,7 @@ entities:
|
||||
~~bonus_weight_*(E-S 加权洗牌权重族)~~ / ~~bonus_cap_*(种类次数上限族)~~:**v2.2 令牌池换代退役(迭代 12 TP0——2026-10-03 追认 b 实施批;洗牌改令牌 Fisher-Yates、池消耗即上限)**
|
||||
curio_quota_{kind}_{tier}: int [0,99](**TP0 令牌配额族**——kind×tier 计数:attack 18/5 · max_hp 17/5 · armor 17/5 · gp 18/5 · heal 0/5 · ap 0/0/3〔common/rare/rarest〕;未列组合=0;档位结构锚 70/25/3=98 张入 schema validate 硬断言〔罕见档原列「道具件 2」随 aux_item 池种退役收窄——台账自决披露项〕)
|
||||
curio_value_attack_rare / curio_value_max_hp_rare / curio_value_armor_rare / curio_value_gp_rare: int [0,999](稀有档大数值对——+2/+10/+2/50);curio_value_heal_rare: int [0,999](恢复件 +6 当即回血——新入池种类 rare 档);MC 策略 316-f(autoplay 1 级最稀有/2 级最左)随批落地
|
||||
**迭代 20 换代(2026-10-11 专长实装批 T2/T4)**:TIERS 四级 common/excellent/rare/legendary(档名映射承袭——旧 rare→excellent、旧 rarest→rare,物理字段改名);TIER_QUOTAS 70/25/9/0=104(T2 直映射 98+T4 凭证投放 6——rap 2/attune_battery 1/time_sand 2/perfect_insurance 1,provisional 归调参批);CURIO_KINDS 7→11;curio_cap_* 每成员已拿上限字段(案②概率上限解耦——99=无上限占位);boss_feat_weight Boss 三选一按档配比权重(100/45/15/5 provisional);新旧字段映射注释随 schema 头注
|
||||
transitional_sync_slots: int [0,9](同调位过渡读数源,默认 1——道具持有上限;正主 = attunement_growth.tres 里程碑谓词表,同调实施批落地后切换读数源)
|
||||
mvp_content: 单行全局默认值即 MVP 行(provisional 归 曲线调参批——迭代 10 扩列批注)
|
||||
extension_reserve: 大奖励形态扩列位已消费(迭代 10 奖励重构批落地三选一/攻击型——本键使命完成退役;冒险结构 OQ2 收口)
|
||||
|
||||
@@ -560,3 +560,8 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/a
|
||||
## 2026-10-10 秘宝匣改名(文档面同步——2026-10-10 天赋树设计会话裁)
|
||||
|
||||
营地柜台「道具工坊」正名「秘宝匣」(树上解锁节点与营地柜台同名):本文档现行正文扫改完毕;**代码标识符(workshop_items 等)与界面文案暂未动**——柜台 UI 名/表行语义连带面归 design/gdd/06-待办清单.md「天赋树结构重构批」实装批办理。
|
||||
|
||||
## 2026-10-11 专长实装批(迭代 20——奇物重设计 T0-T10 全落)
|
||||
|
||||
**换代面**:feats.tres 整表换代(旧 27 件退役→重设计 29 行=基础 27+迁入淬毒刃纹/封印爆裂——订阅行 on/when/do 四级 rarity+cap);效果引擎词表扩签(事件 18/效果类型 30/事实 18/算子 +mod_eq——heal/ac_bonus/damage_reduce/bonus_delta/blast_scaled/preview_area/target_attack_bonus 七新类型+adventure 域计数器跨战账本);池结构案D(四级档位 70/25/9/0=104+曝光槽 2+1+概率上限解耦案②+Boss 按档配比加权抽取);凭证五件入池(刷新券 F-1 修复/储备金袋/同调电池〔choose_battery_target 玩家点选〕/时沙/完美保险);流派标签/档位激活机制退役(FeatsEngine 求值摘除——HUD 特性段降级为逐件行,呈现改法归 06 在册奇物呈现条)。
|
||||
**验证**:冒烟 864/0+run_all 21 套+probe_gate 全绿;MC 同种子重放 30 局逐位复现;新基线 normal/off 100 局通关 0%(master 对照同为 0%=零配基线本相,止步分布 90/10→93/7 可归因曝光槽)。设计权威=奇物.md(反哺点清单见 design/qa/iter20-实现报告-2026-10-11.md)。
|
||||
@@ -44,6 +44,7 @@ var charge_layers: int:
|
||||
# 效果引擎(内容数据驱动扩展批 T1——dig_paid/charge_full 发射面;round_end 转存决策面)
|
||||
var _fx: EffectEngine = null
|
||||
var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策移至 round_end 发射点——grant_ap(transfer) 订阅定夺)
|
||||
var _ap_exhausted_last := false # 上轮 AP 耗尽旗(迭代 20 T1——一滴不剩 round_start when 面)
|
||||
|
||||
|
||||
func set_fx(fx: EffectEngine) -> void:
|
||||
@@ -97,18 +98,27 @@ func request_dig(chain_size: int, clears_map: bool) -> bool:
|
||||
return false
|
||||
var cost := _cost("dig")
|
||||
# dig_paid 发射+施加(内容数据驱动扩展批 T3——付费开掘发起、动作经济闸收对价前:
|
||||
# counter_spend 命中=尝试加收对价并积计数;资源不足走 fallback/耗尽即止=只付常规基础对价)
|
||||
# counter_spend 命中=尝试加收对价并积计数;资源不足走 fallback/耗尽即止=只付常规基础对价。
|
||||
# 迭代 20 T1——counter_add〔双管镐 付费挖格计数〕/ grant_ap〔每第 2 次返 1 AP——对价减免,
|
||||
# mod_eq 闸在行 when〕同面施加)
|
||||
var dig_refund := 0
|
||||
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 String(h.get("kind", "")) == "counter_spend":
|
||||
var extra := int(h.get("value", 1))
|
||||
if _ap >= cost + extra:
|
||||
cost += extra
|
||||
_fx.counter_add(String(h.get("counter_id", "")), 1)
|
||||
elif 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)
|
||||
elif String(h.get("kind", "")) == "grant_ap":
|
||||
dig_refund += int(h.get("value", 0))
|
||||
if _ap < cost:
|
||||
_reject("resource_face")
|
||||
return false
|
||||
if dig_refund > 0:
|
||||
cost = maxi(0, cost - dig_refund) # 返还=对价减免(净费不低于 0——免费开格不产生负对价)
|
||||
return _spend_cost_and_arbitrate(cost, clears_map)
|
||||
|
||||
|
||||
@@ -153,6 +163,15 @@ func request_stop() -> bool:
|
||||
if _phase != Phase.ACTIVE:
|
||||
_reject("phase")
|
||||
return false
|
||||
# dig_stop 发射+施加(迭代 20 T1——停手挂点〔弃图兑换〕:剩余 AP 随载荷外发,counter_add
|
||||
# value_event="ap_left" 取值面〔值=停手时刻真实余量〕;发射先于记账清零)
|
||||
if _fx != null:
|
||||
for h in _fx.dispatch("dig_stop", {"ap_left": _ap}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
var val := int(h.get("value", 0))
|
||||
if String(h.get("value_event", "")) == "ap_left":
|
||||
val = _ap
|
||||
_fx.counter_add(String(h.get("counter_id", "")), val, h.has("set"))
|
||||
_last_stop_ap = _ap
|
||||
_ap = 0
|
||||
_finish_round("stopped", "same_board")
|
||||
@@ -160,6 +179,11 @@ func request_stop() -> bool:
|
||||
return true
|
||||
|
||||
|
||||
# 储备池注入(迭代 20 T4——储备金袋凭证:战首 RAP +N,节点级一次性;任意相位受理=战首注入时 Idle/Active 均合法)
|
||||
func grant_reserve_ap(n: int) -> void:
|
||||
_rap += maxi(0, n)
|
||||
|
||||
|
||||
# 轮末停手转存决策(round_end 发射点调用——编排器授予段尾):transfer=true=剩余 AP 转蓄力层
|
||||
# (上限 3,整备军备面);false=1:1 转存 RAP(缺省)。非停手轮(cleared/耗尽)_last_stop_ap=0
|
||||
# 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。
|
||||
@@ -249,9 +273,15 @@ func _finish_round(path_tag: String, continuation_hint: String) -> void:
|
||||
assert(path_tag != "ap_exhausted" or _ap == 0, "耗尽轮剩余行动点恒为 0(实现层断言兜底)")
|
||||
assert(path_tag != "cleared" or _ap == 0, "清图轮剩余行动点当场清零(不进储备池)")
|
||||
_last_round_end = {"path_tag": path_tag, "continuation_hint": continuation_hint}
|
||||
_ap_exhausted_last = path_tag == "ap_exhausted" # 迭代 20 T1——一滴不剩 when 面(轮始发射读数)
|
||||
_phase = Phase.WAITING
|
||||
|
||||
|
||||
# 上轮 AP 是否耗尽(迭代 20 T1——round_start 发射 payload 读数面;轮始兑现后保持到本轮末)
|
||||
func ap_exhausted_last_round() -> bool:
|
||||
return _ap_exhausted_last
|
||||
|
||||
|
||||
func _reject(source: String) -> void:
|
||||
if source == "phase":
|
||||
push_warning("[行动经济] 动作被拒(%s 相位守卫/资源)" % source)
|
||||
|
||||
+184
-26
@@ -41,7 +41,7 @@ class_name AdventureRunner
|
||||
extends RefCounted
|
||||
|
||||
signal level_started(level_index: int, display_name: String) # 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行
|
||||
signal curio_choice_presented(payload: Dictionary) # 提升三选一停点(非末关通关恰一次;迭代 10 大奖励重构——big_reward_paid 退役)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared}
|
||||
signal curio_choice_presented(payload: Dictionary) # 专长三选一停点(非末关通关恰一次;迭代 20 T5 换代正名——旧「提升三选一」退役,通道白名单后候选恒专长+回血在载荷)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared}
|
||||
signal curio_resolved(result: Dictionary) # 三选一兑付完成(pick_curio/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index}
|
||||
signal treasure_replace_presented(items: Array, incoming: Dictionary) # 同调位满位替换子交互(pick_curio 道具件满位时)——items=现有持有件记录、incoming=新件记录
|
||||
signal adventure_finished(summary: Dictionary) # 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子(+additive keystone_drops 迭代 10)
|
||||
@@ -77,6 +77,13 @@ var _attune_refresh_x := 0 # 刷新件余量(迭代 11 第①
|
||||
var _attune_refresh_y := 0 # Y 冒险周期池(开局参数面注入,用完即无——营地升级行表值)
|
||||
var _curio_paid_refreshes := 0 # 本次停点已付费刷新次数(2026-10-06 用户令对齐商店 MC-1——GP 刷新价曲线输入,随停点创建清零=与商店进店清零同构)
|
||||
var attune_charges := {} # 同调主动件冒险充能账本(迭代 11 第①批 T6——跨关卡共享 dict;settlement use_treasure 扣减直改同引用;断点随 attunement_charges 键入档)
|
||||
var adventure_counters := {} # adventure 域计数器账本(迭代 20 T6——攒层/付费计数等跨战持久件账;效果引擎 rebuild 回灌/战末导出回写共享引用;断点随 adventure_counters 键入档)
|
||||
# ---- 凭证件三本新账(迭代 20 T4——奇物.md 凭证件表新 5 件;F-1 刷新写错账修复承载面)----
|
||||
var _refresh_stock := 0 # 刷新存量账(冒险级——刷新券加仓;**不经 _attune_refresh_x**:X 每停点重置会吞存量=旧写错账缺陷 F-1)
|
||||
var _pending_rap := 0 # 储备金袋待注入(节点级——下一战 RAP +N,战首注入后清零)
|
||||
var _battery_pending := 0 # 同调电池待分配充能(冒险级——choose_battery_target 玩家点选消费,禁自动替选)
|
||||
var _time_sand_queue := 0 # 时沙待注入(回合级——下一战首轮 AP +N,战首入队后清零)
|
||||
var _pending_insurance := 0 # 完美保险待注入(节点级——下一战标错豁免 N 次,战首注入后清零;战内未用即过期)
|
||||
var _save_manager = null # 断点承载注入(迭代 8 卡①——null = 不写断点;模拟/冒烟缺省)
|
||||
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
|
||||
# ---- 三选一停点态(迭代 10 奖励重构批)----
|
||||
@@ -179,6 +186,8 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
|
||||
var bp_taken: Dictionary = bp.get("bonus_taken", {}) as Dictionary
|
||||
for id in _curio_taken.keys():
|
||||
_curio_taken[id] = maxi(0, int(bp_taken.get(id, 0)))
|
||||
for id in bp_taken.keys():
|
||||
_curio_taken[String(id)] = maxi(0, int(bp_taken.get(id, 0))) # 迭代 20 T2——身份维键(relic:<feat_id>)随快照还原
|
||||
# 令牌池余库还原(TP0 v2.2——快照缺键 = 旧版断点,保持满库重建态:旧档只读一版周期的防御口径;
|
||||
# 迭代 12 T1——relic 令牌 feat_id 随还原)
|
||||
var bp_deck = bp.get("bonus_deck", null)
|
||||
@@ -217,6 +226,12 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
|
||||
var bp_pending: Dictionary = bp.get("bonus_pending", {}) as Dictionary
|
||||
_feat_ticket_pity = int(bp.get("feat_ticket_pity", 0)) # T5 扩列——旧档缺键缺省 0(存档兼容)
|
||||
_feat_ticket_sold = int(bp.get("feat_ticket_sold", 0))
|
||||
# 迭代 20 T6/T7——adventure 域计数器回填(缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先)
|
||||
var bp_counters = bp.get("adventure_counters", null)
|
||||
if bp_counters is Dictionary:
|
||||
adventure_counters = (bp_counters as Dictionary).duplicate(true)
|
||||
_refresh_stock = maxi(0, int(bp.get("refresh_stock", 0))) # 迭代 20 T4/T7——旧档缺键缺省 0
|
||||
_battery_pending = maxi(0, int(bp.get("battery_pending", 0)))
|
||||
var bp_result: Dictionary = bp.get("bonus_result", {}) as Dictionary
|
||||
if not bp_pending.is_empty():
|
||||
_pending_next_level = true
|
||||
@@ -263,7 +278,8 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
|
||||
return _start_current_level()
|
||||
|
||||
|
||||
# 三选一停点态归零(冒险开始/结算收束共用;令牌池按表配额重建——池消耗即上限,账本随池重生)
|
||||
# 三选一停点态归零(冒险开始/结算收束共用;令牌池按表配额重建——池消耗即上限,账本随池重生。
|
||||
# 迭代 20 T6——adventure 域计数器账本随冒险开始清零;resume 路径调用后由断点附随件回填)
|
||||
func _reset_curio_state() -> void:
|
||||
_curio_taken = {"attack": 0, "max_hp": 0, "armor": 0, "gp": 0, "ap": 0, "heal": 0}
|
||||
_curio_deck = _build_curio_deck()
|
||||
@@ -273,11 +289,19 @@ func _reset_curio_state() -> void:
|
||||
_bonus_treasure_carry = []
|
||||
_keystone_drops = []
|
||||
_node_curio_pending = {}
|
||||
adventure_counters = {}
|
||||
_refresh_stock = 0
|
||||
_pending_rap = 0
|
||||
_battery_pending = 0
|
||||
_time_sand_queue = 0
|
||||
_pending_insurance = 0
|
||||
|
||||
|
||||
# 令牌池构建(TP0 v2.2——按表 kind×tier 配额展开为持久多重集;构建序确定 = CURIO_KINDS×TIERS 逐层,
|
||||
# 随机性只发生在洗牌消费 reward_pick 封闭流的时刻。迭代 12 T1:规则改写型特性带=relic 活行每件 1 张追加
|
||||
# (rarity common/rare→稀有令牌档、rarest→罕见令牌档——relic 永不入普通档;空表=零追加零扰动))
|
||||
# 随机性只发生在洗牌消费 reward_pick 封闭流的时刻。迭代 12 T1:规则改写型特性带=relic 活行每件 1 张追加。
|
||||
# 迭代 20 T2/T3 四级换代——relic 令牌档 = 行 rarity 直映射(四级词表内直用;旧三档值 transitional
|
||||
# 回退映射 rarest→rare/其余→excellent,T3 行换代后回退分支自然休眠);relic 入普通档随新档位框架
|
||||
# 解禁〔旧「永不入普通档」系 TP0 三档时代约束,四级框架未沿用——奇物.md 池结构节无此限制〕)
|
||||
func _build_curio_deck() -> Array:
|
||||
var deck: Array = []
|
||||
for kind in db.adventure.CURIO_KINDS:
|
||||
@@ -285,7 +309,8 @@ func _build_curio_deck() -> Array:
|
||||
for i in int(db.adventure.curio_quota(String(kind), String(tier))):
|
||||
deck.append({"kind": String(kind), "tier": String(tier)})
|
||||
for row in db.feat_rows():
|
||||
var rtier := "rarest" if String(row.get("rarity", "")) == "rarest" else "rare"
|
||||
var rraw := String(row.get("rarity", ""))
|
||||
var rtier := rraw if db.adventure.TIERS.has(rraw) else ("rare" if rraw == "rarest" else "excellent")
|
||||
deck.append({"kind": "relic", "tier": rtier, "feat_id": String(row.get("feat_id", ""))})
|
||||
return deck
|
||||
|
||||
@@ -409,19 +434,37 @@ func _feat_payload_keys(feat_id: String) -> Dictionary:
|
||||
# 按挂点消费,档位质变由 FeatsEngine 求值)。兑付即从 _curio_deck 摘除该令牌(选中踢池;
|
||||
# 未选候选从未摘牌=自动回池)。道具件不入此体——未知件返回 false 由调用方按渠道名报错。
|
||||
func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = "") -> bool:
|
||||
if kind == "refresh":
|
||||
_attune_refresh_x += value # T6——刷新次数件兑付=X 余量增量(作用三选一界面非角色,不进加算区——registry refresh_boundary)
|
||||
return true
|
||||
if kind == "relic" and db.feat_row(feat_id).is_empty():
|
||||
push_error("[冒险结构] relic 兑付失败——表无此件: %s" % feat_id)
|
||||
return false # 先校验后摘牌(令牌零丢失)
|
||||
if not _consume_deck_token(kind, tier, feat_id):
|
||||
push_error("[冒险结构] 令牌消费失败——牌库无此令牌: %s|%s|%s" % [kind, tier, feat_id])
|
||||
return false
|
||||
if kind == "refresh":
|
||||
_refresh_stock += value # T4 F-1 修复——刷新券入刷新存量账(冒险级;**退役 _attune_refresh_x 写口**:X 每停点 _reset_refresh_x 重置会吞存量=写错账缺陷正主;摘牌先于落账=选中即消尽,旧早退路径零摘牌缺陷同批修正)
|
||||
_curio_taken["refresh"] = int(_curio_taken.get("refresh", 0)) + 1
|
||||
return true
|
||||
if kind == "rap":
|
||||
_pending_rap += value # T4——储备金袋:下一战 RAP +N(节点级,战首注入后清零)
|
||||
_curio_taken["rap"] = int(_curio_taken.get("rap", 0)) + 1
|
||||
return true
|
||||
if kind == "attune_battery":
|
||||
_battery_pending += value # T4——同调电池:待分配充能(choose_battery_target 玩家点选消费——禁自动替选)
|
||||
_curio_taken["attune_battery"] = int(_curio_taken.get("attune_battery", 0)) + 1
|
||||
return true
|
||||
if kind == "time_sand":
|
||||
_time_sand_queue += value # T4——时沙:下一战首轮 AP +N(回合级池增量,不改上限)
|
||||
_curio_taken["time_sand"] = int(_curio_taken.get("time_sand", 0)) + 1
|
||||
return true
|
||||
if kind == "perfect_insurance":
|
||||
_pending_insurance += value # T4——完美保险:下一战标错豁免 N 次(节点级,未用即过期)
|
||||
_curio_taken["perfect_insurance"] = int(_curio_taken.get("perfect_insurance", 0)) + 1
|
||||
return true
|
||||
if kind == "relic":
|
||||
if not _feats.has(feat_id):
|
||||
_feats.append(feat_id)
|
||||
_curio_taken["relic"] = int(_curio_taken.get("relic", 0)) + 1
|
||||
_curio_taken["relic:" + feat_id] = int(_curio_taken.get("relic:" + feat_id, 0)) + 1 # 迭代 20 T2 案②——身份维已拿计数(cap 消费面)
|
||||
return true
|
||||
# 加算形态路由(内容数据驱动扩展批 T5——match 收敛进效果类型:属性值提升四类走
|
||||
# adventure_table.CURIO_APPLY_ROUTES 声明路由(bonus 落点键),加新加算类令牌零代码)
|
||||
@@ -657,9 +700,37 @@ func _reset_refresh_x() -> void:
|
||||
_attune_refresh_x = _attune_refresh_x_total()
|
||||
|
||||
|
||||
# 当前可刷新次数(X 优先 + Y 池——呈现层余量读数口)
|
||||
# 当前可刷新次数(X 优先 + Y 池 + 刷新存量账——呈现层余量读数口;T4 F-1 修复扩第三源)
|
||||
func attune_refresh_left() -> int:
|
||||
return _attune_refresh_x + _attune_refresh_y
|
||||
return _attune_refresh_x + _attune_refresh_y + _refresh_stock
|
||||
|
||||
|
||||
# 刷新存量账读数(T4——刷新券加仓余量单独读面;呈现层与断点消费)
|
||||
func refresh_stock() -> int:
|
||||
return _refresh_stock
|
||||
|
||||
|
||||
# 同调电池充能分配(T4——玩家点选哪件已装配主动秘宝 +N;**禁自动替选**:
|
||||
# 无待分配/件不在装配集/非主动充能件=拒绝零副作用)
|
||||
func choose_battery_target(item_id: String) -> bool:
|
||||
if _battery_pending <= 0:
|
||||
push_error("[冒险结构] 电池分配拒绝——无待分配充能")
|
||||
return false
|
||||
if not _attunement.has(item_id):
|
||||
push_error("[冒险结构] 电池分配拒绝——件不在装配集: %s" % item_id)
|
||||
return false
|
||||
var arow: Dictionary = db.treasure_item_row(item_id)
|
||||
if String(arow.get("form", "")) != "active_charged":
|
||||
push_error("[冒险结构] 电池分配拒绝——非主动充能件: %s" % item_id)
|
||||
return false
|
||||
_battery_pending -= 1
|
||||
attune_charges[item_id] = int(attune_charges.get(item_id, 0)) + 1
|
||||
return true
|
||||
|
||||
|
||||
# 待分配充能读数(呈现层电池选件面数据源)
|
||||
func battery_pending() -> int:
|
||||
return _battery_pending
|
||||
|
||||
|
||||
# 节点停点在途态读回(组合根重 present 用——candidates/aux_detail 值拷贝)
|
||||
@@ -699,12 +770,16 @@ func _refresh_curio_common(paid: bool) -> bool:
|
||||
if paid:
|
||||
_curio_paid_refreshes += 1
|
||||
else:
|
||||
if _attune_refresh_x <= 0 and _attune_refresh_y <= 0:
|
||||
# 免费位消耗序:X(刷新件每停点重给,优先消耗)→ Y(营地冒险周期池)→ 存量账(刷新券加仓,
|
||||
# 迭代 20 T4 F-1 修复新账——跨停点持久)
|
||||
if _attune_refresh_x <= 0 and _attune_refresh_y <= 0 and _refresh_stock <= 0:
|
||||
return false
|
||||
if _attune_refresh_x > 0:
|
||||
_attune_refresh_x -= 1
|
||||
else:
|
||||
elif _attune_refresh_y > 0:
|
||||
_attune_refresh_y -= 1
|
||||
else:
|
||||
_refresh_stock -= 1
|
||||
if not _curio_pending.is_empty():
|
||||
var cands: Array = _draw_curio_candidates("level_clear")
|
||||
_curio_pending["candidates"] = cands.duplicate(true)
|
||||
@@ -780,38 +855,111 @@ func _finish_node_bonus(picked: String, value: int, tier: String = "", feat_id:
|
||||
# 奖励节点/商店只出属性值提升+资源(非 relic 令牌),专长在非 Boss 渠道只以门票形态出现(T5)。
|
||||
# 过滤式而非物理拆池:deck 物理结构/存档键 bonus_deck 混编余库/消耗语义(选中踢池未选回池)原样,
|
||||
# 白名单在候选视图层生效——配额结构锚 TIER_QUOTAS 70/25/3=98 零改动(专长令牌本系活行追加面,在锚外)。
|
||||
# 迭代 20 T2 案D 换代——曝光槽:非 Boss 渠道三选一 = **2 格全池 + 1 格优秀档以上子池**
|
||||
# (rank≥excellent;子池空该格回退全池;池空直通同族兜底);凭证(feat_ticket)不进子池
|
||||
# 〔独立计数体系〕;已拿上限 cap(案②提前退役):taken≥cap 的身份在候选视图剔除〔牌库本体不动,
|
||||
# 拿了才计、刷出不计——_curio_cap_ok〕。
|
||||
func _draw_curio_candidates(channel: String = "reward_node") -> Array:
|
||||
_last_draw_treasure = []
|
||||
var pool := _curio_deck.duplicate()
|
||||
if channel == "level_clear":
|
||||
pool = pool.filter(func(tok): return String((tok as Dictionary).get("kind", "")) == "relic")
|
||||
else:
|
||||
pool = pool.filter(func(tok): return String((tok as Dictionary).get("kind", "")) != "relic")
|
||||
pool = pool.filter(func(tok):
|
||||
var k := String((tok as Dictionary).get("kind", ""))
|
||||
return k != "relic" and k != "feat_ticket")
|
||||
pool = pool.filter(func(tok):
|
||||
var t: Dictionary = tok as Dictionary
|
||||
return _curio_cap_ok(String(t.get("kind", "")), String(t.get("feat_id", ""))))
|
||||
var rg: RandomNumberGenerator = rng.open_stream("reward_pick")
|
||||
if channel == "level_clear":
|
||||
# 迭代 20 T5——Boss 渠道按档配比加权抽取(无放回加权抽:逐席按 boss_feat_weight 掷点,
|
||||
# 身份去重照旧;rarity 语义约束兑付面——奇物.md v3.1,配比数值归调参批)
|
||||
var out_w: Array = []
|
||||
var seen_w: Array = []
|
||||
var want_w := int(db.adventure.curio_candidate_count)
|
||||
while out_w.size() < want_w and not pool.is_empty():
|
||||
var total_w := 0
|
||||
for tok_w in pool:
|
||||
total_w += db.adventure.boss_feat_weight(String((tok_w as Dictionary).get("tier", "")))
|
||||
if total_w <= 0:
|
||||
break
|
||||
var roll_w := rg.randi_range(1, total_w)
|
||||
var pick_i := 0
|
||||
for i_w in pool.size():
|
||||
roll_w -= db.adventure.boss_feat_weight(String((pool[i_w] as Dictionary).get("tier", "")))
|
||||
if roll_w <= 0:
|
||||
pick_i = i_w
|
||||
break
|
||||
var tok_p: Dictionary = pool[pick_i]
|
||||
var ident_p := _token_identity(tok_p)
|
||||
if seen_w.has(ident_p):
|
||||
pool.remove_at(pick_i) # 同身份已出——剔牌重掷(无放回)
|
||||
continue
|
||||
seen_w.append(ident_p)
|
||||
var cand_p := {"kind": String(tok_p.get("kind", "")), "tier": String(tok_p.get("tier", "")), "feat_id": String(tok_p.get("feat_id", ""))}
|
||||
out_w.append(cand_p)
|
||||
pool.remove_at(pick_i)
|
||||
return out_w
|
||||
for i in pool.size():
|
||||
var j := rg.randi_range(i, pool.size() - 1)
|
||||
var tmp = pool[i]
|
||||
pool[i] = pool[j]
|
||||
pool[j] = tmp
|
||||
# 子池(优秀档以上)独立洗牌——同 stream 续消费,确定性保持
|
||||
var sub := pool.filter(func(tok):
|
||||
return db.adventure.tier_rank(String((tok as Dictionary).get("tier", ""))) >= db.adventure.tier_rank(db.adventure.TIER_EXCELLENT))
|
||||
for i in sub.size():
|
||||
var j2 := rg.randi_range(i, sub.size() - 1)
|
||||
var tmp2 = sub[i]
|
||||
sub[i] = sub[j2]
|
||||
sub[j2] = tmp2
|
||||
var out: Array = []
|
||||
var seen: Array = []
|
||||
for tok in pool:
|
||||
var ident := _token_identity(tok)
|
||||
if seen.has(ident):
|
||||
continue
|
||||
seen.append(ident)
|
||||
var cand := {"kind": String((tok as Dictionary).get("kind", "")), "tier": String((tok as Dictionary).get("tier", ""))}
|
||||
if ident.begins_with("relic:"):
|
||||
cand["feat_id"] = String((tok as Dictionary).get("feat_id", ""))
|
||||
out.append(cand)
|
||||
if out.size() >= int(db.adventure.curio_candidate_count):
|
||||
# 收集器:全池序取 n_full 格(普通档打底)→ 子池序取 1 格(曝光槽)→ 子池空回退全池续取
|
||||
var want := int(db.adventure.curio_candidate_count)
|
||||
var n_full := maxi(1, want - 1) if want >= 2 else want
|
||||
while out.size() < want:
|
||||
var src: Array = pool
|
||||
if out.size() >= n_full and not sub.is_empty():
|
||||
src = sub # 曝光槽位——优先子池
|
||||
var placed := false
|
||||
for tok in src:
|
||||
var ident := _token_identity(tok)
|
||||
if seen.has(ident):
|
||||
continue
|
||||
seen.append(ident)
|
||||
var cand := {"kind": String((tok as Dictionary).get("kind", "")), "tier": String((tok as Dictionary).get("tier", ""))}
|
||||
if ident.begins_with("relic:"):
|
||||
cand["feat_id"] = String((tok as Dictionary).get("feat_id", ""))
|
||||
out.append(cand)
|
||||
placed = true
|
||||
break
|
||||
if not placed:
|
||||
if out.size() >= n_full and not sub.is_empty() and not pool.is_empty():
|
||||
sub = [] # 子池枯竭——曝光槽回退全池续取
|
||||
continue
|
||||
break # 全池枯竭——出齐剩余(池空直通同族兜底)
|
||||
# T5 门票注入(非 Boss 渠道唯一专长入口——替换末位候选占一席,候选数不变;判定含 pity 清零记账)
|
||||
if channel == "reward_node" and not out.is_empty() and _feat_ticket_consider(rg):
|
||||
out[out.size() - 1] = {"kind": "feat_ticket", "tier": "rare"}
|
||||
out[out.size() - 1] = {"kind": "feat_ticket", "tier": "excellent"}
|
||||
return out
|
||||
|
||||
|
||||
# 已拿上限判定(迭代 20 T2 案②——身份维度:kind 走 curio_cap、relic 走 feats 行 cap 字段;
|
||||
# taken 只在兑付时 +1〔拿了才计〕、刷出不计)。缺省放行=白名单外 kind 兜底。
|
||||
func _curio_cap_ok(kind: String, feat_id: String = "") -> bool:
|
||||
var cap := 99
|
||||
if kind == "relic":
|
||||
var row: Dictionary = db.feat_row(feat_id)
|
||||
cap = int(row.get("cap", 1)) if not row.is_empty() else 1
|
||||
return _curio_taken.get("relic:" + feat_id, 0) < cap
|
||||
if not db.adventure.CURIO_KINDS.has(kind):
|
||||
return true
|
||||
cap = int(db.adventure.curio_cap(kind))
|
||||
return int(_curio_taken.get(kind, 0)) < cap
|
||||
|
||||
|
||||
# —— 专长获取门票(T5 迭代 15 卡②——非 Boss 渠道的专长入口;四口径契约④:拿到即开不可囤积/
|
||||
# 罕见只在概率不在价格/一局限购一次/计步保底连续 5 展示未出第 6 必出——计数=展示次数、刷新
|
||||
# 重展计入、节点与商店共享、触发清零、限购满不再触发。调用即一次展示判定与记账)——
|
||||
@@ -937,8 +1085,15 @@ func _start_current_level() -> bool:
|
||||
_runner.set_feats(_feats) # 迭代 12 T1——特性持有集下传(begin_battle enc.feats 求值面)
|
||||
_runner.set_tree_rules(_tree_rules) # 迭代 12 T10——树 rule 键集下传(A/B 面消费)
|
||||
if _shop_mode == "pool":
|
||||
# 商店共享池注入(迭代 11 第②批 T3——库存账本引用+消费 handler 双下传;legacy 模式零注入零扰动)
|
||||
_runner.set_shop_pool(_curio_deck, Callable(self, "_consume_pool_purchase"), Callable(self, "_feat_ticket_consider"))
|
||||
# 商店共享池注入(迭代 11 第②批 T3——库存账本引用+消费 handler 双下传;legacy 模式零注入零扰动。
|
||||
# 迭代 20 T2——cap 检查回调四参注入)
|
||||
_runner.set_shop_pool(_curio_deck, Callable(self, "_consume_pool_purchase"), Callable(self, "_feat_ticket_consider"), Callable(self, "_curio_cap_ok"))
|
||||
_runner.set_adventure_counters(adventure_counters) # 迭代 20 T6——adventure 域计数器共享引用下传
|
||||
# 迭代 20 T4——凭证件三账注入(节点级/回合级一次性:注入即清零,未用即过期=寿命档语义)
|
||||
_runner.set_voucher_grants(_pending_rap, _time_sand_queue, _pending_insurance)
|
||||
_pending_rap = 0
|
||||
_time_sand_queue = 0
|
||||
_pending_insurance = 0
|
||||
# 三选一道具件跨关携带注入(迭代 10)——迭代 11 第①批 T7 退役:aux 卡出池后 carry 恒空集,
|
||||
# 注入面随停点渠道退役拆除(容器字段保留=替换子交互机械惰性承载,零投放零行为)
|
||||
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
|
||||
@@ -1087,6 +1242,9 @@ func _snapshot_breakpoint() -> void:
|
||||
"bonus_pending": _curio_pending.duplicate(true), # 三选一停点态(空 = 无停点——选择停点可中断契约)
|
||||
"feat_ticket_pity": _feat_ticket_pity, # T5 门票保底计数(扩列向后兼容——旧档缺键缺省 0)
|
||||
"feat_ticket_sold": _feat_ticket_sold, # T5 门票限购计数(同上)
|
||||
"adventure_counters": adventure_counters.duplicate(true), # 迭代 20 T6/T7——adventure 域计数器随断点(续玩跨战账不丢)
|
||||
"refresh_stock": _refresh_stock, # 迭代 20 T4/T7——刷新存量账随断点(F-1 新账)
|
||||
"battery_pending": _battery_pending, # 迭代 20 T4/T7——电池待分配充能随断点
|
||||
"bonus_result": _curio_result.duplicate(true), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
|
||||
"bonus_aux_carry": _bonus_treasure_carry.duplicate(true), # 道具件跨关携带(注入前中断防静默丢失)
|
||||
"keystone_drops": _keystone_drops.duplicate(true), # 逐杀掉落明细(摘要归因续接——迭代 10)
|
||||
|
||||
@@ -76,6 +76,17 @@ var _flag_consumed := {} # 兑付资格以雷格实例为键
|
||||
var attunement: Array = [] # 装备中同调件 item_id 集
|
||||
var _attune_charges_ref: Dictionary = {} # 充能账本共享引用(迭代 11 第①批 T6——use_treasure 消费直改)
|
||||
var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数(进度呈现取数面;积点账本=效果引擎计数器 dig_charge_bank——行 triggers counter_spend 订阅同账本)
|
||||
# ---- 效果引擎扩签运行态(迭代 20 专长实装批 T1——新效果类型施加面;空订阅零扰动)----
|
||||
var _fx_hero_ac_base := -1 # 主角 AC 基线快照(enc.hero_armor_class;-1=表值兜底未解析)
|
||||
var _fx_ac_round := 0 # ac_bonus 本轮累积器(封印桩形态——round_start 清零)
|
||||
var _fx_ac_battle := 0 # ac_bonus 本战累积器(石肤形态——cap 封顶,begin_battle 清零)
|
||||
var _fx_adventure_counters_ref: Dictionary = {} # adventure 域计数器共享引用(T6——attune_charges 先例:begin_battle 回灌 rebuild、battle 末 counters_export 回写)
|
||||
var _fx_bonus_ref: Dictionary = {} # 冒险加算区共享引用(bonus_delta 战末汇入——血税双向账)
|
||||
var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 本战 bonus_delta 累积(战末并入 _fx_bonus_ref 后清零)
|
||||
var _fx_new_resource_types := {} # 本战已见资源类型集(地质学家 fact new_resource_type 判定——battle 域)
|
||||
var _fx_pending_reveals: Array[Dictionary] = [] # 揭示效果待兑队列(见血开窍/斩首学者/地质学家/跳格采掘——授予段末统一兑付,防轮 facts 迭代中追加重入)
|
||||
var _fx_revealing := false # fx 揭示兑付中标记(_on_cell_revealed 缓冲旁路——fx 揭示的奖励由兑付点直接入账,防二次缓冲二次兑付)
|
||||
var _fx_insurance := 0 # 完美保险余量(迭代 20 T4——本战标错豁免次数;消费于 flag_miss 首见判定)
|
||||
# 特性运行态(内容数据驱动扩展批 T3——relic 持有集经 enc.feats 注入:效果行为=数据行 triggers 经
|
||||
# 效果引擎分发;魔矢每战首次计数 battle 域。空集=零扰动)
|
||||
var _feat_first_dig_used := false
|
||||
@@ -84,7 +95,7 @@ var _feat_first_dig_used := false
|
||||
var _fx: EffectEngine
|
||||
|
||||
|
||||
# 事件发射薄面(16 发射点统一入口——订阅空=零循环零扰动;确定性同步分发零 RNG)
|
||||
# 事件发射薄面(18 发射点统一入口——订阅空=零循环零扰动;确定性同步分发零 RNG)
|
||||
func _fx_emit(event: String, payload: Dictionary) -> Array:
|
||||
if _fx == null:
|
||||
return []
|
||||
@@ -92,13 +103,73 @@ func _fx_emit(event: String, payload: Dictionary) -> Array:
|
||||
|
||||
|
||||
# round_start 发射+施加(内容数据驱动扩展批 T3——grant_ap mode:queue=当轮入队,行动经济轮切换
|
||||
# 即兑现;计数器 round_start 重置点在 dispatch 内生效。调用点两处:begin_battle 首轮 / 轮始段续轮)
|
||||
# 即兑现;计数器 round_start 重置点在 dispatch 内生效。调用点两处:begin_battle 首轮 / 轮始段续轮。
|
||||
# 迭代 20 T1——payload 扩 ap_exhausted_last〔一滴不剩 when 面〕+ac_bonus 本轮累积器清零)
|
||||
func _emit_round_start() -> void:
|
||||
for h in _fx_emit("round_start", {"round": round_index}):
|
||||
_fx_ac_round = 0
|
||||
_sync_hero_ac()
|
||||
for h in _fx_emit("round_start", {"round": round_index,
|
||||
"ap_exhausted_last": action_economy.ap_exhausted_last_round()}):
|
||||
if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
|
||||
action_economy.queue_ap_delta(int(h.get("value", 0)))
|
||||
|
||||
|
||||
# ac_bonus 累积后同步主角 AC(石肤/封印桩消费面——defender_ac 经 set_hero_ac_override 单点;
|
||||
# 基线 = enc 口径,-1(表值兜底态)时解析为表值后叠加,守恒口径与 begin_battle 注入一致)
|
||||
func _sync_hero_ac() -> void:
|
||||
if _fx_hero_ac_base < 0:
|
||||
_fx_hero_ac_base = int(db.hero.armor_class)
|
||||
_damage.set_hero_ac_override(_fx_hero_ac_base + _fx_ac_round + _fx_ac_battle)
|
||||
|
||||
|
||||
# ac_bonus 施加(迭代 20 T1——mode=round 本轮累积器〔封印桩,round_start 清零〕/
|
||||
# mode=battle 本战累积器〔石肤,cap 封顶;缺省 battle〕;cap=0/缺省=无上限)
|
||||
func _apply_ac_bonus(h: Dictionary) -> void:
|
||||
var mode := String(h.get("mode", "battle"))
|
||||
var cap := int(h.get("cap", 0))
|
||||
if mode == "round":
|
||||
_fx_ac_round += int(h.get("value", 0))
|
||||
else:
|
||||
_fx_ac_battle = _fx_ac_battle + int(h.get("value", 0)) if cap <= 0 else mini(cap, _fx_ac_battle + int(h.get("value", 0)))
|
||||
_sync_hero_ac()
|
||||
|
||||
|
||||
# 揭示效果入队(fx_reveal 掷格兑付延后到授予段末——防轮 facts 迭代中追加重入;见血开窍/
|
||||
# 斩首学者〔经伤害控制器 fx_reveal_request 回调〕/地质学家/跳格采掘共用)
|
||||
func _fx_enqueue_reveal() -> void:
|
||||
_fx_pending_reveals.append({"round": round_index})
|
||||
|
||||
|
||||
# 揭示效果兑付(授予段末统一——fx_reveal 封闭流从未揭示安全格掷 1 格全揭示;
|
||||
# 信息红线=只揭真格〔雷格不入候选〕;空候选=零兑付留归因行;揭示奖励经 _grant_routed
|
||||
# 正向路由直接入账——_fx_revealing 旁路 _on_cell_revealed 缓冲防二次兑付)
|
||||
func _flush_pending_reveals(ri: int) -> void:
|
||||
if _fx_pending_reveals.is_empty():
|
||||
return
|
||||
var board: BoardData = minesweeper.current_board()
|
||||
while not _fx_pending_reveals.is_empty():
|
||||
_fx_pending_reveals.pop_back()
|
||||
var cands: Array = []
|
||||
for c in board.all_cells():
|
||||
if not minesweeper.is_revealed(c) and not board.is_mine(c):
|
||||
cands.append(c)
|
||||
if cands.is_empty():
|
||||
_emit_row("fx_reveal", "", Vector2i(-1, -1), ri, "揭示落空(无未揭示安全格)", 0, {}, "")
|
||||
continue
|
||||
var rg: RandomNumberGenerator = rng.open_stream("fx_reveal")
|
||||
var cell: Vector2i = cands[rg.randi_range(0, cands.size() - 1)]
|
||||
_fx_revealing = true
|
||||
var ok: bool = minesweeper.fx_reveal(cell)
|
||||
_fx_revealing = false
|
||||
if not ok:
|
||||
_emit_row("fx_reveal", "", Vector2i(-1, -1), ri, "揭示落空(格子不可揭)", 0, {}, "")
|
||||
continue
|
||||
var snap = _reward_economy.query_full_snapshot(cell)
|
||||
if snap is Dictionary:
|
||||
_grant_routed(snap, "positive", "fx_reveal", cell, ri, "fx_reveal")
|
||||
_emit_row("fx_reveal", "", cell, ri, "专长揭示", 0, {}, "fx_reveal")
|
||||
|
||||
|
||||
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数)
|
||||
func _fx_fact(fact: String):
|
||||
match fact:
|
||||
@@ -118,6 +189,9 @@ func _fx_fact(fact: String):
|
||||
if bool(fevt.get("hit", false)):
|
||||
hits_n += 1
|
||||
return hits_n
|
||||
"hp_ratio":
|
||||
var hu: Dictionary = _enemy_system.hero_unit()
|
||||
return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(0-100——when lte 算子 int 截断比较口径)
|
||||
return null
|
||||
|
||||
|
||||
@@ -238,29 +312,45 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
# modify 窗口、偏运=reward_roll bias_rewards;value 行字段 value_from 定影,零 begin_battle 注入)
|
||||
# 特性运行态(内容数据驱动扩展批 T3——enc.feats 携带持有集:效果行为=数据行 triggers 经效果
|
||||
# 引擎分发;流派档位=FeatsEngine 纯函数求值两运行态注伤害控制器。魔矢每战首次开格计数同步归零)
|
||||
# 迭代 20 T1——扩签运行态就位:AC 基线快照、累积器清零、adventure 计数器/加算区共享引用接收
|
||||
_fx_hero_ac_base = int(enc.get("hero_armor_class", -1))
|
||||
_fx_ac_round = 0
|
||||
_fx_ac_battle = 0
|
||||
_fx_new_resource_types = {}
|
||||
_fx_pending_reveals = []
|
||||
_fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
var bonus_ref = enc.get("bonus_ref", null)
|
||||
_fx_bonus_ref = bonus_ref if bonus_ref is Dictionary else {}
|
||||
var counters_ref = enc.get("adventure_counters", null)
|
||||
_fx_adventure_counters_ref = counters_ref if counters_ref is Dictionary else {}
|
||||
_damage.fx_reveal_request = Callable(self, "_fx_enqueue_reveal")
|
||||
_damage.fx_ac_request = Callable(self, "_apply_ac_bonus")
|
||||
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
|
||||
# 迭代 20 T3 换代(V14 旧实现摘除)——流派档位质变随重设计退役(奇物.md v1「不设流派标签、
|
||||
# 不设档位机制」):FeatsEngine 求值与 rider_tier 注入拆除,散件单体效果=唯一形态
|
||||
_damage.rider_tier_bonus = 0
|
||||
_damage.rider_tier_crit = false
|
||||
_feat_first_dig_used = false
|
||||
# 树 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
|
||||
# 效果引擎订阅重建(内容数据驱动扩展批 T1——每战从持有集/装备集重建,中途获得下一战生效;
|
||||
# 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放)
|
||||
# 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放。
|
||||
# 迭代 20 T6——adventure 域计数器从共享引用回灌〔跨战持久〕)
|
||||
_fx.rebuild(feat_ids, attunement, tree_rules)
|
||||
_fx.counters_seed(_fx_adventure_counters_ref)
|
||||
# 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现
|
||||
# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后
|
||||
# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
|
||||
var ap_bonus: int = int(enc.get("hero_ap_bonus", 0))
|
||||
var ap_bonus: int = int(enc.get("hero_ap_bonus", 0)) + int(enc.get("voucher_time_sand", 0)) # 迭代 20 T4——时沙同通道(首轮池增量,不改上限)
|
||||
if ap_bonus > 0:
|
||||
action_economy.queue_ap_delta(ap_bonus)
|
||||
# 迭代 20 T4——储备金袋(节点级 RAP 注入)与完美保险(标错豁免次数)接收
|
||||
var voucher_rap: int = int(enc.get("voucher_rap", 0))
|
||||
if voucher_rap > 0:
|
||||
action_economy.grant_reserve_ap(voucher_rap)
|
||||
_fx_insurance = int(enc.get("voucher_insurance", 0))
|
||||
# 工兵晨令直调拆除(内容数据驱动扩展批 T4——round_start grant_ap(mode:queue) 订阅行承载,
|
||||
# when round_index ≤ effective_rounds 行字段 value_from 定影;发射位先于轮切换=当轮兑现)
|
||||
# round_start 发射+施加(内容数据驱动扩展批 T3——发射位先于行动经济轮切换:树规则 grant_ap
|
||||
@@ -413,7 +503,9 @@ func _on_cell_revealed(evt: Dictionary) -> void:
|
||||
# 轮 facts 缓冲不设相位守卫——轮末最后一挖的揭示事件在 request_dig 翻相之后才到(事件序:
|
||||
# cell_revealed 批 → flag_judged 批 → round_end),phase==Active 守卫会丢最后一挖的事实;
|
||||
# initial 通道事件(install 初始展开——封锁格)resource=false,进入授予段被资格检查自然跳过。
|
||||
if battle_result == "":
|
||||
# fx_reveal 通道事件旁路缓冲(迭代 20 T1——专长揭示的奖励由 _flush_pending_reveals 兑付点
|
||||
# 直接入账,此处缓冲会导致二次兑付)。
|
||||
if battle_result == "" and not _fx_revealing:
|
||||
if bool(evt.get("resource", false)):
|
||||
# 身份快照随缓冲捕获(批次隔离——轮内刷新后 mapping 已换批,授予必须用缓冲期身份,
|
||||
# 防旧图已挖格在轮末错拿新图同坐标身份)。雷格同样携带(雷安同池——负向兑付的来源格)
|
||||
@@ -463,10 +555,22 @@ func _settle() -> void:
|
||||
_emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "")
|
||||
continue
|
||||
# dig_reveal 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:
|
||||
# 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢)
|
||||
# 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢。
|
||||
# 迭代 20 T1——payload 扩三事实:is_edge_cell〔边陲矿工〕/is_blank_zero〔跳格采掘〕/
|
||||
# new_resource_type〔地质学家——快照 cell_type 首见判定,见后集登记〕)
|
||||
var board_b: BoardData = minesweeper.current_board()
|
||||
var snap_pre = evt.get("reward_snapshot")
|
||||
var ctype := String((snap_pre as Dictionary).get("cell_type", "")) if snap_pre is Dictionary else ""
|
||||
var is_new_type := ctype != "" and not _fx_new_resource_types.has(ctype)
|
||||
if is_new_type:
|
||||
_fx_new_resource_types[ctype] = true
|
||||
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)})
|
||||
"terrain_kind": _harmful_variants(evt.get("variant_ids", []) as Array),
|
||||
"is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0
|
||||
or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1,
|
||||
"is_blank_zero": board_b.digit_of(evt.cell) == 0,
|
||||
"new_resource_type": is_new_type})
|
||||
var pos_m := 0
|
||||
var neg_m := 0
|
||||
var combo_cap := -1
|
||||
@@ -483,6 +587,10 @@ func _settle() -> void:
|
||||
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)
|
||||
# 揭示效果入队(迭代 20 T1——地质学家/跳格采掘 reveal_tiles 订阅面;授予段末
|
||||
# _flush_pending_reveals 统一掷格兑付)
|
||||
if not _hits_of(dr_hits, "reveal_tiles").is_empty():
|
||||
_fx_enqueue_reveal()
|
||||
# 追加产出(深挖钻头 v2——本格双份制:grant_material 命中=配额内追加第二份;标签随行)
|
||||
for h in dr_hits:
|
||||
if String(h.get("kind", "")) == "grant_material":
|
||||
@@ -510,17 +618,45 @@ func _settle() -> void:
|
||||
# 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴),
|
||||
# 标中不发(旗已随挖掘擦除、不入轮末判定基线)。
|
||||
# mine_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:
|
||||
# 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器)
|
||||
# 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器。
|
||||
# 迭代 20 T1——发射前移+payload 扩 dug_under_flag〔破封一击〕;新增施加面:
|
||||
# damage_reduce〔安全帽耗层减伤——value_counter 层值,一次清零〕/ heal〔血蜜〕/
|
||||
# blast_scaled〔陷阱爆发——雷伤×倍率全体敌〕/ bonus_delta〔血税〕/ reveal_tiles〔见血开窍〕)
|
||||
var mine_power := int((snap as Dictionary).get("power", 0))
|
||||
var mh_hits := _fx_emit("mine_hit", {"cell": evt.cell, "round": ri,
|
||||
"damage": int((snap as Dictionary).get("power", 0))})
|
||||
"damage": mine_power, "dug_under_flag": bool(evt.get("dug_under_flag", false)),
|
||||
"hp_ratio": int(round(100.0 * float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))))})
|
||||
var halve_pct := 0
|
||||
var mine_reduce := 0
|
||||
for h in mh_hits:
|
||||
if String(h.get("kind", "")) == "damage_mult":
|
||||
halve_pct += int(h.get("value", 0))
|
||||
match String(h.get("kind", "")):
|
||||
"damage_mult":
|
||||
halve_pct += int(h.get("value", 0))
|
||||
"damage_reduce":
|
||||
if h.has("value_counter") and String(h.get("value_counter", "")) != "":
|
||||
mine_reduce += _fx.counter(String(h.get("value_counter", ""))) # 安全帽——层值减免
|
||||
else:
|
||||
mine_reduce += int(h.get("value", 0)) # 背水——定值减免
|
||||
"heal":
|
||||
var hu_heal: Dictionary = _enemy_system.hero_unit()
|
||||
hu_heal["hp"] = mini(int(hu_heal.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_heal.get("max_hp", 1))))
|
||||
"bonus_delta":
|
||||
var axis := String(h.get("axis", ""))
|
||||
if _fx_bonus_delta.has(axis):
|
||||
_fx_bonus_delta[axis] += int(h.get("delta", 0))
|
||||
"blast_scaled":
|
||||
var blast_v := maxi(int(db.settlement.damage_floor), int(float(mine_power) * float(h.get("mult", 1))))
|
||||
var bs_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发", "board",
|
||||
blast_v, DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {})
|
||||
_submit(bs_t, false)
|
||||
if mine_reduce > 0:
|
||||
snap["power"] = maxi(0, mine_power - mine_reduce) # 减伤落快照(负向兑付按减后值——安全帽每层减 1)
|
||||
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)
|
||||
if not _hits_of(mh_hits, "reveal_tiles").is_empty():
|
||||
_fx_enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付)
|
||||
_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":
|
||||
@@ -538,22 +674,30 @@ func _settle() -> void:
|
||||
if String(vrow.get("effect", "")) != "flat_damage":
|
||||
continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面)
|
||||
# 地形变异伤害单据(伤害管线单据化批 2026-09-24)——无类型必中,下限保底在控制器管线内
|
||||
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)
|
||||
# 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)})
|
||||
# 迭代 20 T1——damage_taken 发射前移(单据开立前):damage_reduce〔背水——hp_ratio when
|
||||
# 面〕先收集再按减后值开单;counter_add〔保险丝/石肤〕+ ac_bonus〔石肤〕照常施加;
|
||||
# grant_material〔荆棘/地脉〕移到单据提交后(与迁移前兑付值序一致——收益不因减伤变化)
|
||||
var dt_amount := int(vrow.get("effect_value", 0))
|
||||
var dt_hits := _fx_emit("damage_taken", {"kind": "terrain", "amount": dt_amount,
|
||||
"source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid),
|
||||
"hp_ratio": int(round(100.0 * float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))))})
|
||||
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", "")))
|
||||
"damage_reduce":
|
||||
dt_amount = maxi(0, dt_amount - int(h.get("value", 0)))
|
||||
"ac_bonus":
|
||||
_apply_ac_bonus(h)
|
||||
"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)
|
||||
var vt := _damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \
|
||||
dt_amount, DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)})
|
||||
_submit(vt, false)
|
||||
for h in dt_hits:
|
||||
if 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", "")))
|
||||
for evt in _facts_flags:
|
||||
_judge_flag(evt, ri)
|
||||
# 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌
|
||||
@@ -599,17 +743,27 @@ func _settle() -> void:
|
||||
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,
|
||||
# 迭代 20 T9——source_class 中性名(旧件名 feat_magma_body 随熔火之躯退役;per-counter 反打通道保留泛用面)
|
||||
var mg_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_blast_counter",
|
||||
"积层反噬 ×%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)
|
||||
# 迭代 20 T1——round_end 新施加面:grant_gp〔爆破会计/收盘——escrow 战斗域缓冲,随 gp_dropped 上抛〕/
|
||||
# grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始加成同面〕
|
||||
for h in re_fx:
|
||||
if String(h.get("kind", "")) == "grant_gp":
|
||||
escrow += int(h.get("value", 0))
|
||||
_emit_row("effect", "fx_grant_gp", Vector2i(-1, -1), ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": escrow}, "")
|
||||
elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue":
|
||||
action_economy.queue_ap_delta(int(h.get("value", 0)))
|
||||
action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer"))
|
||||
if _facts_path == "cleared":
|
||||
_grant_clear_reward(ri)
|
||||
_flush_pending_reveals(ri) # 迭代 20 T1——揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入)
|
||||
_damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界)
|
||||
# ② 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦)
|
||||
_emit_composition_effects("hero", ri)
|
||||
@@ -912,29 +1066,43 @@ func _judge_flag(evt: Dictionary, ri: int) -> void:
|
||||
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 通道)
|
||||
# channel blast value_from magnitude;每标中一面旗全体存活敌吃 N 群伤,无类型走 blast 通道。
|
||||
# 迭代 20 T1——封印爆裂〔迁入专长〕同面消费:行 triggers open_ticket channel 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",
|
||||
var fbt := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_flag_burst", "封印爆裂", "board",
|
||||
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 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)
|
||||
var harmful := false
|
||||
for vid_n in vids_n:
|
||||
var vrow_n: Dictionary = db.variation.row_by_id(String(vid_n))
|
||||
if String(vrow_n.get("effect", "")) == "flat_damage":
|
||||
harmful = true
|
||||
break
|
||||
if harmful:
|
||||
board.clear_variants(n)
|
||||
_emit_row("terrain_neutralized", "feat_purge_sapper", n, ri, "净化工兵·地形中和", 0, {}, "")
|
||||
# 迭代 20 T1——标中新施加面:counter_add〔血税 标中+1 攻——bonus_delta atk 轴〕/
|
||||
# ac_bonus〔封印桩 本轮 AC+N〕/ preview_area〔示警烟火 3×3 资源类型预告〕/
|
||||
# remove_mines〔封印匠 每第 3 次该危险格中立移除——mod_eq 闸在行 when〕。
|
||||
# (净化工兵 rewrite_board 分支随老 27 件退役摘除——效果类型同批除名,T1 落法 commit 留痕)
|
||||
for h in fh_hits:
|
||||
match 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)
|
||||
"ac_bonus":
|
||||
_apply_ac_bonus(h)
|
||||
"bonus_delta":
|
||||
var axis_f := String(h.get("axis", ""))
|
||||
if _fx_bonus_delta.has(axis_f):
|
||||
_fx_bonus_delta[axis_f] += int(h.get("delta", 0))
|
||||
"remove_mines":
|
||||
var removed: Array = minesweeper.remove_mines_in([cell])
|
||||
if not removed.is_empty():
|
||||
_emit_row("mine_neutralized", "fx_seal_smith", cell, ri, "封印匠·危险格中立移除", 0, {}, "")
|
||||
"preview_area":
|
||||
var board_p: BoardData = minesweeper.current_board()
|
||||
var types: Array = []
|
||||
for n in board_p.neighbors8(cell):
|
||||
var n_snap = _reward_economy.query_full_snapshot(n)
|
||||
if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))):
|
||||
types.append(String((n_snap as Dictionary).get("cell_type", "")))
|
||||
if treasure_inventory != null and not types.is_empty():
|
||||
treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell))
|
||||
_emit_row("fx_preview_area", "", cell, ri, "示警烟火·周围资源类型", 0, {"types": types}, "")
|
||||
# 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格;
|
||||
# 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由)
|
||||
for h in fh_hits:
|
||||
@@ -951,6 +1119,12 @@ func _judge_flag(evt: Dictionary, ri: int) -> void:
|
||||
if _flag_miss_seen.has(cell):
|
||||
return # 每格实例一次去重(归因行与负向兑付同键——防逐轮惩罚轰炸)
|
||||
_flag_miss_seen[cell] = true
|
||||
# 完美保险(迭代 20 T4——本战下一次标错不破坏完美资格:_perfect_hit_false 回退=本次标错
|
||||
# 对完美谓词无痕;只保一次/件,节点级用完即止)
|
||||
if _fx_insurance > 0:
|
||||
_fx_insurance -= 1
|
||||
_perfect_hit_false -= 1
|
||||
_emit_row("insurance_used", "", cell, ri, "完美保险·标错豁免", 0, {"left": _fx_insurance}, "")
|
||||
var hl := ""
|
||||
if _perfect_hit_false == 1:
|
||||
hl = "perfect_broken" # 本板完美谓词已破的归因锚
|
||||
@@ -1023,8 +1197,13 @@ func _roll_round_actions() -> void:
|
||||
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,
|
||||
# 迭代 20 T1——target_attack_bonus 施加面(处刑台:敌血量阈值沿挂单位字典标记,
|
||||
# 主角攻击面 _resolve_hero_attack 通用消费——fx_atk_bonus 键加算进 base_value)
|
||||
var eht_hits := _fx_emit("enemy_hp_threshold", {"unit": String(u.enemy_id), "pct": pct_v,
|
||||
"is_boss": String(encounter.get("node_type", "")) == "boss"})
|
||||
for h in eht_hits:
|
||||
if String(h.get("kind", "")) == "target_attack_bonus":
|
||||
u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0))
|
||||
var entries: Array = _enemy_system.rollable_skills(u)
|
||||
if _enemy_system.basic_streak(u) >= pity_m:
|
||||
var specials: Array = _enemy_system.special_skills(u)
|
||||
@@ -1077,8 +1256,9 @@ func _combat_segment(ri: int) -> void:
|
||||
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", int(h.get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri,
|
||||
# 迭代 20 T9——source_class 中性名(旧件名 feat_chain_arrows 随连珠矢退役;attack_segments 兑付通道保留泛用面)
|
||||
var ct := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments",
|
||||
"连击分段", "board", int(h.get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri,
|
||||
minesweeper.hero_pos(), "pierce")
|
||||
_damage.submit(ct)
|
||||
# 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。
|
||||
@@ -1243,6 +1423,24 @@ func player_request_refresh() -> bool:
|
||||
|
||||
func _battle_over(result: String) -> void:
|
||||
battle_result = result
|
||||
# battle_won 发射+施加(迭代 20 T1——战地包扎 heal 订阅面;败北不发射。heal=英雄单位回血
|
||||
# 〔封顶单位上限〕,战后生命经 hero_hp_left 回写冒险账本——时点在捕获前=回血计入带入下一层)
|
||||
if result == "victory":
|
||||
for h in _fx_emit("battle_won", {"round": round_index}):
|
||||
if String(h.get("kind", "")) == "heal":
|
||||
var hu_w: Dictionary = _enemy_system.hero_unit()
|
||||
hu_w["hp"] = mini(int(hu_w.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_w.get("max_hp", 1))))
|
||||
# bonus_delta 战末汇入(迭代 20 T1——血税双向账:本战累积并入冒险加算区共享引用后清零)+
|
||||
# adventure 域计数器导出回写(T6——共享引用覆写,attune_charges 先例)
|
||||
if _fx_bonus_ref != null and not _fx_bonus_ref.is_empty():
|
||||
for axis in _fx_bonus_delta.keys():
|
||||
if _fx_bonus_ref.has(axis):
|
||||
_fx_bonus_ref[axis] = int(_fx_bonus_ref.get(axis, 0)) + int(_fx_bonus_delta[axis])
|
||||
_fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
if _fx != null and not _fx_adventure_counters_ref.is_empty():
|
||||
var exported: Dictionary = _fx.counters_export()
|
||||
for cid in exported.keys():
|
||||
_fx_adventure_counters_ref[cid] = int(exported[cid])
|
||||
# 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提)
|
||||
var hero_hp_left := int(_enemy_system.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层")
|
||||
var gp_dropped := _roll_gp_drop() # 战斗掉落(GP+商店房实施批——escrow 随此值上抛即弃)
|
||||
|
||||
@@ -111,8 +111,11 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
if not adv.CURIO_KINDS.has(kind):
|
||||
push_diag.call("error", "shelf_dangling_kind", "ShopTable.pool_shelf kind %s 不在 AdventureTable.CURIO_KINDS(共享池牌库外种)" % kind)
|
||||
continue
|
||||
# TP0 令牌池——货架件须为有限令牌种(配额 >0;种类次数上限退役后库存=剩余令牌数)
|
||||
var quota := int(adv.curio_quota(kind, adv.TIER_COMMON)) + int(adv.curio_quota(kind, adv.TIER_RARE)) + int(adv.curio_quota(kind, adv.TIER_RAREST))
|
||||
# TP0 令牌池——货架件须为有限令牌种(配额 >0;种类次数上限退役后库存=剩余令牌数。
|
||||
# 迭代 20 T2 四级换代——逐档累计遍历 TIERS 词表,不再点名档位常量)
|
||||
var quota := 0
|
||||
for tier_v in adv.TIERS:
|
||||
quota += int(adv.curio_quota(kind, String(tier_v)))
|
||||
if quota < 1:
|
||||
push_diag.call("error", "shelf_zero_quota", "ShopTable.pool_shelf kind %s 令牌配额为 0(牌库空种入货架 = 库存无锚)" % kind)
|
||||
|
||||
|
||||
@@ -102,16 +102,25 @@ func _fx_dispatch(event: String, payload: Dictionary) -> void:
|
||||
_fx_hits(event, payload)
|
||||
|
||||
|
||||
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数:charge_layers 编排器每轮注入)
|
||||
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数:charge_layers 编排器每轮注入。
|
||||
# 迭代 20 T1——hp_ratio 补入〔背水 when 面,敌技 damage_taken 载荷不带血比例时回落〕;
|
||||
# fx_reveal_request=揭示效果回调〔斩首学者 kill 订阅面——编排器注入,入队授予段末兑付〕)
|
||||
func _fx_fact(fact: String):
|
||||
match fact:
|
||||
"charge_layers":
|
||||
return charge_layers
|
||||
"dig_streak":
|
||||
return dig_streak
|
||||
"hp_ratio":
|
||||
var hu: Dictionary = units.hero_unit()
|
||||
return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(when lte int 截断口径)
|
||||
return null
|
||||
|
||||
|
||||
var fx_reveal_request: Callable = Callable() # 揭示效果回调(编排器注入——kill 发射点 reveal_tiles 订阅面)
|
||||
var fx_ac_request: Callable = Callable() # ac_bonus 回调(编排器注入——石肤 AC 累积单点同步)
|
||||
|
||||
|
||||
# per 取数(per 字段解析——载荷同名字段优先,回落补充事实面;add_ticket_rider/attack_bonus/
|
||||
# shield/open_ticket 的倍数承载:贡献值 = value × per 读数,cap 对乘积封顶)
|
||||
func _per_value(name: String, payload: Dictionary) -> int:
|
||||
@@ -317,14 +326,27 @@ func _resolve_hero_attack(t: Dictionary) -> void:
|
||||
# modify(mode:clamp_bonus) 订阅命中已并入上方 delta 合计;mode 路由=主角攻击面专享,
|
||||
# 棋盘克制直伤窗只消费无模式 delta——棋盘不吃毒面与迁移前一致)
|
||||
var base_value := float(t.base_value)
|
||||
# 单位标记加伤(迭代 20 T1——处刑台 target_attack_bonus 挂单位字典 fx_atk_bonus 键;
|
||||
# 阈值沿发射点已写入,本处通用消费——对该敌伤害加算)
|
||||
base_value += float(int(target.get("fx_atk_bonus", 0)))
|
||||
# 按序改写族第五拍:矿工重锤增伤面(交换的正向侧)
|
||||
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
|
||||
# 加法族:整备军备「每层下轮首段 +1」(层数×value 加算)——attack_bonus per charge_layers。
|
||||
# 迭代 20 T1——value_counter 扩展〔疼痛记忆/弃图兑换/陷阱本能——rider 值=计数器账面,
|
||||
# 值读于施加时点=命中序先行 rider 后清零行〕
|
||||
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))
|
||||
var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload)
|
||||
if h.has("value_counter") and String(h.get("value_counter", "")) != "":
|
||||
contrib = _fx.counter(String(h.get("value_counter", "")))
|
||||
base_value += float(contrib)
|
||||
# 计数器施加(迭代 20 T1——attack_commit 命中行内 counter_add〔用后清零行〕,命中序=声明序)
|
||||
for h in ac_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)
|
||||
# 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载)
|
||||
if spike_ready:
|
||||
for h in ac_hits:
|
||||
@@ -362,12 +384,16 @@ func _resolve_hero_attack(t: Dictionary) -> void:
|
||||
# 以穿刺类型弹射本体 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", "")) == "reveal_tiles" and fx_reveal_request.is_valid():
|
||||
fx_reveal_request.call() # 斩首学者——击杀揭示 1 格(编排器入队,授予段末兑付)
|
||||
continue
|
||||
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)),
|
||||
# 迭代 20 T9——source_class 中性名(旧件名 feat_pierce_shot 随贯穿射击退役;overflow 通道保留泛用面)
|
||||
var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "fx_overflow",
|
||||
"贯穿弹射", "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)
|
||||
@@ -377,7 +403,8 @@ func _resolve_hero_attack(t: Dictionary) -> void:
|
||||
# 重复授予由状态容器自行守卫)
|
||||
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)
|
||||
# 迭代 20 T9——source_class 中性名(旧件名 feat_armor_crack 随碎甲震击退役;apply_status 通道保留泛用面)
|
||||
effects.apply_effect(String(h.get("status_id", "")), {"source_class": "fx_apply_status"}, -1)
|
||||
payload["cracked"] = true
|
||||
var label := _attack_label(String(t.display), crit, discount)
|
||||
if hero_rider > 0:
|
||||
@@ -399,12 +426,22 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
|
||||
var total: int = maxi(int(db.settlement.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区
|
||||
# damage_taken 发射+施加(内容数据驱动扩展批 T3——kind=enemy_skill;铁壁整备减伤订阅面:
|
||||
# shield 加法族合计 value×per(charge_layers),稳健拓荒树规则同面(rebuild 合成订阅);
|
||||
# 减后过下限保底)
|
||||
# 减后过下限保底。迭代 20 T1——damage_reduce〔背水 hp_ratio when〕/counter_add〔保险丝/
|
||||
# 石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 _fx_fact 补充事实面回落提供)
|
||||
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", "")), {})
|
||||
match String(h.get("kind", "")):
|
||||
"shield":
|
||||
shield += int(h.get("value", 0)) * _per_value(String(h.get("per", "")), {})
|
||||
"damage_reduce":
|
||||
total = maxi(0, total - int(h.get("value", 0)))
|
||||
"ac_bonus":
|
||||
if fx_ac_request.is_valid():
|
||||
fx_ac_request.call(h) # 石肤 AC 累积归编排器单点(set_hero_ac_override 消费面)
|
||||
"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)
|
||||
if shield > 0:
|
||||
total = maxi(int(db.settlement.damage_floor), total - shield)
|
||||
units.apply_damage(hero, total)
|
||||
|
||||
+65
-15
@@ -16,10 +16,11 @@
|
||||
class_name EffectEngine
|
||||
extends RefCounted
|
||||
|
||||
## 事件词汇(16 个发射点——需求卡片 §3.1 定稿;4 个登记暂不做事件不入本表)
|
||||
## 事件词汇(迭代 20 专长实装批 T1 扩签——16→18:+dig_stop 停手挂点〔弃图兑换〕
|
||||
## +battle_won 战斗胜利〔战地包扎〕;原 16 发射点不动)
|
||||
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"]
|
||||
"round_end", "reward_roll", "treasure_used", "charge_full", "dig_stop", "battle_won"]
|
||||
|
||||
## 扫雷侧事件集(🔴 流派扫雷触发铁律新形态:每流派 ≥1 行订阅本集事件——schema 加载校验。
|
||||
## 原 MINE_HOOKS 的同义迁移:挖掘/标旗/轮末结算面事件)
|
||||
@@ -31,27 +32,49 @@ const MINE_EVENTS := ["dig_reveal", "dig_paid", "flag_hit", "mine_hit", "flag_se
|
||||
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"]
|
||||
"counter_spend", "reveal_tiles", "remove_mines", "refresh_pick",
|
||||
"bias_rewards", "preview",
|
||||
# 迭代 20 专长实装批 T1 扩签六型(奇物.md 基础件表 27 件声明清单——落法映射 commit 留痕):
|
||||
# heal=回复生命〔血蜜/战地包扎〕;ac_bonus=护甲加值累积器〔封印桩本轮/石肤本场 cap〕;
|
||||
# damage_reduce=受伤减免〔背水/安全帽〕;bonus_delta=冒险加算区增量〔血税 双向——battle 末汇入
|
||||
# runner.bonus〕;blast_scaled=雷罚标定群伤〔陷阱爆发——值=事件伤害×mult〕;
|
||||
# preview_area=资源类型预告〔示警烟火——3×3 邻域资源类型,不揭安危位置〕;
|
||||
# target_attack_bonus=单位标记加伤〔处刑台——敌血量阈值挂单位字典,主角攻击面通用消费〕
|
||||
"heal", "ac_bonus", "damage_reduce", "bonus_delta", "blast_scaled", "preview_area",
|
||||
"target_attack_bonus"]
|
||||
# 注:rewrite_board 已除名(迭代 20 T9——净化工兵随老 27 件退役,V14 连带清理;
|
||||
# 编排器消费分支 T1 先行摘除)
|
||||
|
||||
## fact 条件词汇(封闭枚举 9 项——需求卡片 §四定稿)
|
||||
## fact 条件词汇(封闭枚举——迭代 20 T1 扩签 9→17:payload 同名键直读面〔fact 取值序 payload
|
||||
## 优先——effect_engine._fact_value 既有机制〕。new_resource_type=首开资源类型〔地质学家〕;
|
||||
## is_edge_cell=边缘格〔边陲矿工〕;is_blank_zero=空白连锁〔跳格采掘〕;dug_under_flag=被挖格
|
||||
## 当时有旗〔破封一击〕;ap_exhausted_last=上轮 AP 耗尽〔一滴不剩〕;hp_ratio=生命比例〔背水
|
||||
## 06 前置条兑付〕;amount=单次伤害量〔保险丝〕;ap_left=停手剩余 AP〔弃图兑换〕)
|
||||
const FACTS := ["matchup", "is_first_dig_of_battle", "terrain_kind", "target_armor_class",
|
||||
"flags_count", "charge_layers", "nest_count", "round_index", "counter"]
|
||||
"flags_count", "charge_layers", "nest_count", "round_index", "counter",
|
||||
"new_resource_type", "is_edge_cell", "is_blank_zero", "dug_under_flag",
|
||||
"ap_exhausted_last", "hp_ratio", "amount", "ap_left", "pct"]
|
||||
|
||||
## when 算子词汇(封闭——fact 查询面落法随批登记:mod_zero=计数器取模判满〔掘进蓄能〕、
|
||||
## advantageous=克制矩阵正面判定〔淬毒刃纹〕、gte_ref/lt_ref=与另一计数器比较〔深挖钻头配额〕、
|
||||
## ends_with=后缀匹配〔对某甲族的泛条件〕。均系映射表现状条件装进 9 项 fact 的查询面落法,
|
||||
## 代拍板白名单覆盖,commit 留痕)
|
||||
## ends_with=后缀匹配〔对某甲族的泛条件〕、mod_eq=取模余数判定〔双管镐每第 2 次——value=模数
|
||||
## ref=余数;dispatch 预评估口径〔when 于命中前读计数器现值〕下「每第 N 次」系用 ref=N-1 表达〕。
|
||||
## 均系映射表现状条件装进 fact 的查询面落法,代拍板白名单覆盖,commit 留痕)
|
||||
const OPS := ["eq", "neq", "gt", "gte", "lt", "lte", "ends_with", "has", "mod_zero",
|
||||
"advantageous", "gte_ref", "lt_ref"]
|
||||
"advantageous", "gte_ref", "lt_ref", "mod_eq"]
|
||||
|
||||
## when 条件键白名单(trigger 行 when 数组元素键)
|
||||
const WHEN_KEYS := ["fact", "op", "value", "id", "ref", "value_from"]
|
||||
|
||||
## do 效果键白名单(trigger 行 do 字典键——零裸 flat 同族护栏:kind 外键全部参数引用面)
|
||||
## do 效果键白名单(trigger 行 do 字典键——零裸 flat 同族护栏:kind 外键全部参数引用面。
|
||||
## 迭代 20 T1 扩签四键:value_event=事件载荷取值键名〔爆破会计——counter_add 值=本轮挖格花费
|
||||
## payload.ap_cost〕;value_counter=计数器取值键〔疼痛记忆/弃图兑换——attack rider 值=账面层值〕;
|
||||
## domain=计数器寿命域〔battle=战斗域重建清零〔缺省〕/adventure=跨战持久随 battle 末导出〕;
|
||||
## axis=bonus_delta 加算区轴向)
|
||||
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"]
|
||||
"reset_at", "set", "status_id", "label", "fallback", "target_kinds", "trade_atk", "trade_hit",
|
||||
"value_event", "value_counter", "domain", "axis"]
|
||||
|
||||
## 主动件分发通道(不经事件订阅——刷新/显影/移除是玩家主动动作;use_treasure 按 kind 分发)
|
||||
const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"]
|
||||
@@ -60,6 +83,8 @@ const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "r
|
||||
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 _adventure_counter_ids: Array[String] = [] # domain=adventure 计数器名单(T6——重建时收集、不清零;
|
||||
# battle 末 export → 冒险账本、begin_battle seed 回灌)
|
||||
var db: ConfigDb # 矩阵/表读取(advantageous 算子消费)——可空(纯计数器用法零依赖)
|
||||
|
||||
|
||||
@@ -72,8 +97,8 @@ func _init(db_ref: ConfigDb = null) -> void:
|
||||
## 声明收集重置点(round_start)。
|
||||
func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
|
||||
_triggers.clear()
|
||||
_counters.clear()
|
||||
_reset_on_round_start.clear()
|
||||
_adventure_counter_ids.clear()
|
||||
if db != null and db.feats != null:
|
||||
for row in db.feats.rows:
|
||||
var fid := String(row.get("feat_id", ""))
|
||||
@@ -99,6 +124,8 @@ func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
|
||||
"posture_bulwark":
|
||||
_push_trigger("tree", rk, {"on": "damage_taken",
|
||||
"do": {"kind": "shield", "value": 1}}, {})
|
||||
# 计数器账本重建(T6——battle 域清零照旧;adventure 域由 counters_seed 回灌〔rebuild 后调用〕)
|
||||
_counters = {}
|
||||
|
||||
|
||||
func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary) -> void:
|
||||
@@ -127,6 +154,11 @@ func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary)
|
||||
var cid := String(do_d.counter_id)
|
||||
if not _reset_on_round_start.has(cid):
|
||||
_reset_on_round_start.append(cid)
|
||||
# 寿命域收集(T6——domain=adventure 计数器跨战持久:重建不清零、battle 末导出回冒险账本)
|
||||
if do_d.has("counter_id") and String(do_d.get("domain", "")) == "adventure":
|
||||
var aid := String(do_d.counter_id)
|
||||
if not _adventure_counter_ids.has(aid):
|
||||
_adventure_counter_ids.append(aid)
|
||||
|
||||
|
||||
## 订阅查询面(发射点缺省行为仲裁用——如 round_end 无 transfer 订阅时走 RAP 转存)
|
||||
@@ -181,6 +213,21 @@ func counter_spend(counter_id: String, value: int) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
## adventure 域计数器导出(T6——battle 末调用,快照回冒险账本;仅 domain=adventure 名单内键)
|
||||
func counters_export() -> Dictionary:
|
||||
var out := {}
|
||||
for cid in _adventure_counter_ids:
|
||||
out[cid] = int(_counters.get(cid, 0))
|
||||
return out
|
||||
|
||||
|
||||
## adventure 域计数器回灌(T6——rebuild 后由发射宿主调用:冒险账本值回填名单内键,缺省 0;
|
||||
## 名单=本次 rebuild 装载行声明 domain=adventure 的 counter_id 集)
|
||||
func counters_seed(seed: Dictionary) -> void:
|
||||
for cid in _adventure_counter_ids:
|
||||
_counters[cid] = maxi(0, int(seed.get(cid, 0)))
|
||||
|
||||
|
||||
# ---- when 判定(条件谓词——零随机全确定) ----
|
||||
|
||||
func _when_ok(when: Array, payload: Dictionary, facts: Callable) -> bool:
|
||||
@@ -225,10 +272,13 @@ func _cond_ok(c: Dictionary, payload: Dictionary, facts: Callable) -> bool:
|
||||
return false
|
||||
if actual is Array:
|
||||
return not (actual as Array).is_empty()
|
||||
return String(actual) != ""
|
||||
return actual != null and String(actual) != "" # null 安全(迭代 20 T2 修正——敌技伤害载荷无 terrain_kind 时事实面回落 null,判「不在场」而非抛错;候选流变化后 devour 件首次被持有而暴露的潜伏缺陷)
|
||||
"mod_zero":
|
||||
var m := int(val)
|
||||
return m > 0 and int(actual) % m == 0
|
||||
"mod_eq":
|
||||
var me_m := int(val)
|
||||
return me_m > 0 and int(actual) % me_m == int(c.get("ref", 0))
|
||||
"advantageous":
|
||||
# 克制矩阵正面判定(matchup = "{attack_type}_vs_{armor}"——矩阵值 > 0 = 克制成立)
|
||||
return _counter_delta_of(String(actual)) > 0
|
||||
@@ -326,7 +376,7 @@ static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array,
|
||||
## rewrite_board、apply_status——发射点逐条执行。
|
||||
static func combo_of(kind: String) -> String:
|
||||
match kind:
|
||||
"add_ticket_rider", "attack_bonus", "grant_material", "counter_add", "shield":
|
||||
"add_ticket_rider", "attack_bonus", "grant_material", "counter_add", "shield", "heal", "ac_bonus", "damage_reduce", "bonus_delta":
|
||||
return "additive"
|
||||
"grant_ap":
|
||||
return "additive" # mode:transfer 由发射点即时执行族承载(转移非加法)
|
||||
@@ -338,6 +388,6 @@ static func combo_of(kind: String) -> String:
|
||||
return "ordered_write"
|
||||
"counter_spend":
|
||||
return "ordered_spend"
|
||||
"open_ticket", "rewrite_board", "apply_status":
|
||||
"open_ticket", "apply_status", "blast_scaled":
|
||||
return "immediate"
|
||||
return "immediate" # 默认即时执行(每条命中各自执行一次)
|
||||
@@ -72,6 +72,7 @@ var _shop_pool_mode := false # pool 模式旗(set_shop_pool 注
|
||||
var _pool_deck: Array = [] # 共享池令牌库共享引用(AdventureRunner._curio_deck——库存=剩余令牌数单点)
|
||||
var _pool_consume := Callable() # 池消费 handler(AdventureRunner._consume_pool_purchase——_apply_curio_pick 同口写加算区+账本+摘牌)
|
||||
var _pool_ticket := Callable() # 门票展示判定 handler(T5——AdventureRunner._feat_ticket_consider;节点与商店共享计数)
|
||||
var _pool_cap_check := Callable() # cap 检查回调(迭代 20 T2——AdventureRunner._curio_cap_ok;案②提前退役)
|
||||
var _shop_shelf: Array = [] # 当前货架(进店抽 3/买后全重抽——令牌字典 {"kind","tier"} 序,同 kind 去重每 kind 至多一卡)
|
||||
var _shop_visit_buys := 0 # 本次进店已购次数(A 链定价输入——离开清零)
|
||||
var _shop_paid_refreshes := 0 # 本次进店已付费刷新次数(MC-1——GP 刷新价曲线输入,离开清零)
|
||||
@@ -482,9 +483,17 @@ func _start_battle(node: Dictionary) -> bool:
|
||||
"attune_charges": _attune_charges,
|
||||
"feats": _feats,
|
||||
"tree_rules": _tree_rules,
|
||||
"bonus_ref": bonus,
|
||||
"adventure_counters": _adventure_counters_ref,
|
||||
"voucher_rap": _voucher_rap,
|
||||
"voucher_time_sand": _voucher_time_sand,
|
||||
"voucher_insurance": _voucher_insurance,
|
||||
"node_type": String(node.get("type", ""))}):
|
||||
push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
|
||||
return false
|
||||
_voucher_rap = 0 # 迭代 20 T4——节点级一次性:注入即消费(战斗未启动则保留至下次注入前不清)
|
||||
_voucher_time_sand = 0
|
||||
_voucher_insurance = 0
|
||||
return true
|
||||
|
||||
|
||||
@@ -614,6 +623,29 @@ func set_tree_rules(rules: Array) -> void:
|
||||
_tree_rules = rules.duplicate()
|
||||
|
||||
|
||||
# adventure 域计数器共享引用注入(迭代 20 T6——AdventureRunner 冒险级账本引用下传;
|
||||
# begin_battle enc.adventure_counters 携带进效果引擎 rebuild 回灌与战末 counters_export 回写——
|
||||
# **共享引用不复制**,attune_charges 同款;缺省空 dict=零扰动)
|
||||
var _adventure_counters_ref: Dictionary = {}
|
||||
|
||||
|
||||
func set_adventure_counters(ref: Dictionary) -> void:
|
||||
_adventure_counters_ref = ref
|
||||
|
||||
|
||||
# 凭证件三账注入(迭代 20 T4——储备金袋 RAP/时沙 首轮 AP/完美保险 标错豁免;节点级一次性,
|
||||
# battle begin 消费后归零;缺省 0=零扰动)
|
||||
var _voucher_rap := 0
|
||||
var _voucher_time_sand := 0
|
||||
var _voucher_insurance := 0
|
||||
|
||||
|
||||
func set_voucher_grants(rap: int, time_sand: int, insurance: int) -> void:
|
||||
_voucher_rap = maxi(0, rap)
|
||||
_voucher_time_sand = maxi(0, time_sand)
|
||||
_voucher_insurance = maxi(0, insurance)
|
||||
|
||||
|
||||
# 同调主动件充能账本注入(迭代 11 第①批 T6——共享 dict 引用直改;能力件骑库存面注入:
|
||||
# 差额注入防续玩双份——resume 路径 restore_inventory 已含断点内 attune 记录)
|
||||
func set_attune_charges(charges: Dictionary) -> void:
|
||||
@@ -736,10 +768,12 @@ func player_request_refresh() -> bool:
|
||||
|
||||
# 注入(AdventureRunner._start_current_level 唯一调用方——pool 模式专用):令牌库共享引用 +
|
||||
# 池消费 handler。legacy 模式不注入(_shop_pool_mode 恒 false——off 态零 RNG 零池消费零扰动)。
|
||||
func set_shop_pool(deck_ref: Array, consume_handler: Callable, ticket_consider: Callable = Callable()) -> void:
|
||||
# 迭代 20 T2——cap 检查回调(案②提前退役:cap 满 kind 剔出货架候选视图)。
|
||||
func set_shop_pool(deck_ref: Array, consume_handler: Callable, ticket_consider: Callable = Callable(), cap_check: Callable = Callable()) -> void:
|
||||
_pool_deck = deck_ref
|
||||
_pool_consume = consume_handler
|
||||
_pool_ticket = ticket_consider # T5——门票展示判定(AdventureRunner._feat_ticket_consider;同流取数复用 shop_draw scope)
|
||||
_pool_cap_check = cap_check
|
||||
_shop_pool_mode = true
|
||||
|
||||
|
||||
@@ -760,6 +794,8 @@ func _shop_draw_shelf() -> void:
|
||||
var sellable: Array = []
|
||||
for row in db.shop_pool_rows():
|
||||
var shelf_kind := String(row.get("kind_id", ""))
|
||||
if _pool_cap_check.is_valid() and not bool(_pool_cap_check.call(shelf_kind, "")):
|
||||
continue # 迭代 20 T2 案②——cap 满提前退役(货架候选视图剔除;牌库本体不动)
|
||||
for t in _pool_deck:
|
||||
var tok: Dictionary = t as Dictionary
|
||||
if String(tok.get("kind", "")) == shelf_kind:
|
||||
@@ -784,7 +820,7 @@ func _shop_draw_shelf() -> void:
|
||||
# T5 门票注入(非 Boss 渠道唯一专长入口——货架与节点共享保底计数;判定含 pity 记账,替换末位占一席。
|
||||
# 取数复用本函数 shop_draw scope 的 stream——零新 scope,确定性序列不漂移)
|
||||
if _pool_ticket.is_valid() and not _shop_shelf.is_empty() and bool(_pool_ticket.call(stream)):
|
||||
_shop_shelf[_shop_shelf.size() - 1] = {"kind": "feat_ticket", "tier": "rare"}
|
||||
_shop_shelf[_shop_shelf.size() - 1] = {"kind": "feat_ticket", "tier": "excellent"} # 迭代 20 T2——档名四级换代(旧 rare 语义=优秀)
|
||||
# 商店保底退役(迭代 15 卡② T4——渠道轴定位裁决:商店渠道只出属性值提升+资源,专长在非 Boss
|
||||
# 渠道只以门票形态出现(奇物.md v2.7/A4 断言)。迭代 12 T6「货架零特性挤卡换吞噬流件」简化口径
|
||||
# 与渠道白名单直接冲突(devour=规则改写型=专长系),随渠道分离退役;专长商店入口=T5 门票。
|
||||
|
||||
@@ -332,6 +332,26 @@ func _board_face_dig(c: Vector2i) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
# fx_reveal 效果通道揭示(迭代 20 专长实装批 T1——订阅类「揭示 1 格」效果的棋盘面执行口:
|
||||
# 不经行动经济〔零 AP 消耗零相位要求〕、不要求邻接〔任意揭示〕、只受理安全格〔信息红线=只揭真格,
|
||||
# 雷格/已开/出界/移除格=拒绝零副作用〕。连锁与揭示事件与 _do_dig 同构(chain 通道复用),资源兑付归
|
||||
# 战斗结算 _flush_pending_reveals 直接入账——channel=fx_reveal 供归因)
|
||||
func fx_reveal(c: Vector2i) -> bool:
|
||||
if _board == null or not _board.in_bounds(c) or is_revealed(c) or _board.is_mine(c) or _removed.has(c):
|
||||
return false
|
||||
var chain := _chain_closure(c, _flags)
|
||||
for rc in _chain_reveal_order(c, _flags):
|
||||
if _revealed.has(rc):
|
||||
continue
|
||||
_revealed[rc] = {"kind": "safe", "digit": _board.digit_of(rc)}
|
||||
cell_revealed.emit({
|
||||
"cell": rc, "kind": "safe", "digit": _board.digit_of(rc),
|
||||
"variant_ids": _board.variant_ids(rc), "channel": "fx_reveal", "resource": true,
|
||||
"dug_under_flag": false,
|
||||
})
|
||||
return true
|
||||
|
||||
|
||||
# 连锁闭包:μX.[O∪{c} ⊆ X ∧ ∀x∈X:(x∈S ∧ num(x)=0 ⇒ N₈(x)∩S∖G ⊆ X)]——连锁不越旗
|
||||
func _chain_closure(start: Vector2i, flag_set: Dictionary) -> Dictionary:
|
||||
var chain := {start: true}
|
||||
|
||||
@@ -15,7 +15,7 @@ class_name RngServiceCore
|
||||
extends Node
|
||||
|
||||
# 用途标签封闭词表(并流前七流名全保留;新增用途走扩签通道 = 改代码 + 评审——清单封闭语义由「流」改「标签」)
|
||||
const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop", "node_map", "reward_pick", "shop_draw"] # node_map = 关卡节点图生成 v0.4 扩签(M-3 迭代 5 T5);reward_pick = 提升三选一加权洗牌扩签(M-3 迭代 10 奖励重构批——每关大奖励抽签恰一 scope:E-S weight-key 每候选一取,标签不参与派生只进事件日志,派生/黄金向量零改动);shop_draw = 商店房货架抽签扩签(M-4 迭代 11 第②批 T1——每次抽 3〔进店+每次买后重抽〕恰一 scope,与 reward_pick 分签防取数序互扰;shop=off 零消费零扰动)
|
||||
const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop", "node_map", "reward_pick", "shop_draw", "fx_reveal"] # node_map = 关卡节点图生成 v0.4 扩签(M-3 迭代 5 T5);reward_pick = 提升三选一加权洗牌扩签(M-3 迭代 10 奖励重构批——每关大奖励抽签恰一 scope:E-S weight-key 每候选一取,标签不参与派生只进事件日志,派生/黄金向量零改动);shop_draw = 商店房货架抽签扩签(M-4 迭代 11 第②批 T1——每次抽 3〔进店+每次买后重抽〕恰一 scope,与 reward_pick 分签防取数序互扰;shop=off 零消费零扰动);fx_reveal = 专长订阅类揭示效果掷格(迭代 20 T1——见血开窍/斩首学者等「揭示 1 格」,与 reward_pick 分签防取数序互扰;只揭真格=安全格信息红线承载)
|
||||
|
||||
# 分段表达式书写(数据配置 R3 实现级规约——禁 ≥2^63 完整字面量)
|
||||
const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15
|
||||
|
||||
@@ -24,16 +24,22 @@ extends Resource
|
||||
## 类内异质性:金币与回复是即时兑付,不进持有集、不持续生效。)
|
||||
## 三选一提升池 kind 词表(封闭——断点附随件与牌库剔除态依赖它;扩件走本表+registry 修订)。
|
||||
## treasure_item 池种退役(迭代 11 第①批 T7——同调批出池);heal 恢复件新入池(v2.2 令牌池追认 b)。
|
||||
const CURIO_KINDS: Array[String] = ["attack", "max_hp", "armor", "gp", "ap", "heal", "refresh"] # T6 扩第七类(refresh=资源类成员——作为奇物授予的三选一重抽次数,作用对象=三选一界面非角色;见 registry refresh_boundary)
|
||||
## 令牌档位词表(v2.2 三档位——稀有度命名对齐 MC 策略判读序 316-f:1 级最稀有=rarest 优先)
|
||||
const TIERS: Array[String] = ["common", "rare", "rarest"]
|
||||
## 迭代 20 T4——资源凭证五件入池:rap 储备金袋/attune_battery 同调电池/time_sand 时沙/
|
||||
## perfect_insurance 完美保险(配额暂 0——T4 落路由同批给投放数,防无路由 kind 出候选)。
|
||||
const CURIO_KINDS: Array[String] = ["attack", "max_hp", "armor", "gp", "ap", "heal", "refresh",
|
||||
"rap", "attune_battery", "time_sand", "perfect_insurance"] # T6 扩第七类(refresh=资源类成员——作为奇物授予的三选一重抽次数,作用对象=三选一界面非角色;见 registry refresh_boundary)
|
||||
## 令牌档位词表(迭代 20 T2 换代——跨层四级稀有度框架〔01-核心循环 v1.5 档名映射:普通→普通、
|
||||
## 稀有→优秀、罕见→稀有、传说=新增头奖位〕;1 级最稀有=legendary 优先)
|
||||
const TIERS: Array[String] = ["common", "excellent", "rare", "legendary"]
|
||||
const TIER_COMMON := "common"
|
||||
const TIER_EXCELLENT := "excellent"
|
||||
const TIER_RARE := "rare"
|
||||
const TIER_RAREST := "rarest"
|
||||
## 档位配额结构锚(v2.2 追认 b 定稿——validate() 硬断言;改总量=重裁非调参)。
|
||||
## 罕见档 5→3:原「道具件 2」席位随 treasure_item 池种退役(迭代 11 第①批 T7,落地晚于追认 b)收窄,
|
||||
## 池总量 100→98——TP0 台账自决+人工清单披露项。
|
||||
const TIER_QUOTAS := {"common": 70, "rare": 25, "rarest": 3}
|
||||
const TIER_LEGENDARY := "legendary"
|
||||
## 档位配额结构锚(迭代 20 换代——T2 四级直映射 70/25/3/0;T4 凭证四件入稀有档投放 +6
|
||||
## (rap 2/battery 1/时沙 2/保险 1)→ 稀有 9、总量 104。已裁换代授权内的 provisional 投放
|
||||
## (卡文 T2「四级配额表重构」+Q3 单迭代全量),偏离 GDD 占位记录随代拍板追认单呈报;
|
||||
## validate() 硬断言;此后改总量=重裁非调参)。
|
||||
const TIER_QUOTAS := {"common": 70, "excellent": 25, "rare": 9, "legendary": 0}
|
||||
|
||||
# —— 通关部分回血(固定型,非末关通关兑付)——
|
||||
@export var clear_heal_pct: int = 20 # [0,100] knob——回复生效上限的百分比,封顶上限不回满
|
||||
@@ -44,31 +50,55 @@ const TIER_QUOTAS := {"common": 70, "rare": 25, "rarest": 3}
|
||||
@export var shop_refresh_cost_base: int = 20 # [1,999] knob(MC-1 provisional——GP 刷新基价)
|
||||
@export var shop_refresh_cost_curve: float = 1.8 # [1.0,4.0] knob(MC-1 provisional——按已付费刷新数幂曲线,A 链同族)
|
||||
|
||||
# —— 令牌配额(kind × tier 计数;未列组合 = 0——heal 只入稀有/ap 只入罕见,v2.2 档位构成)——
|
||||
# —— 令牌配额(kind × tier 计数;未列组合 = 0。迭代 20 T2 四级换代:旧 rare→excellent、
|
||||
# 旧 rarest→rare 物理承袭〔70/25/3 零偏离〕+凭证四 kind 占位 rare 档配额 0〔T4 落路由同批投放〕)——
|
||||
@export var curio_quota_attack_common: int = 18 # [0,99]
|
||||
@export var curio_quota_attack_rare: int = 5 # [0,99]
|
||||
@export var curio_quota_attack_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_max_hp_common: int = 17 # [0,99]
|
||||
@export var curio_quota_max_hp_rare: int = 5 # [0,99]
|
||||
@export var curio_quota_max_hp_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_armor_common: int = 17 # [0,99]
|
||||
@export var curio_quota_armor_rare: int = 5 # [0,99]
|
||||
@export var curio_quota_armor_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_gp_common: int = 18 # [0,99]
|
||||
@export var curio_quota_gp_rare: int = 5 # [0,99]
|
||||
@export var curio_quota_heal_rare: int = 3 # [0,99]——恢复件(原 5;T6 挤占 2 席位予 refresh——用户 2026-10-05 裁方案B,总量 98 锚不变)
|
||||
@export var curio_quota_refresh_rare: int = 2 # [0,99]——刷新次数件(T6 新入池资源类成员;provisional 归曲线调参批)
|
||||
@export var curio_quota_ap_rarest: int = 3 # [0,99]——AP 件(罕见档;道具件 2 席位随池种退役收窄)
|
||||
@export var curio_quota_gp_excellent: int = 5 # [0,99]
|
||||
@export var curio_quota_heal_excellent: int = 3 # [0,99]——恢复件(原 5;T6 挤占 2 席位予 refresh——用户 2026-10-05 裁方案B,总量 98 锚不变)
|
||||
@export var curio_quota_refresh_excellent: int = 2 # [0,99]——刷新次数件(T6 新入池资源类成员;provisional 归曲线调参批)
|
||||
@export var curio_quota_ap_rare: int = 3 # [0,99]——AP 件(稀有档;旧罕见档物理承袭)
|
||||
@export var curio_quota_rap_rare: int = 2 # [0,99]——储备金袋(T4 投放 provisional 归调参批)
|
||||
@export var curio_quota_attune_battery_rare: int = 1 # [0,99]——同调电池(T4 投放 provisional 归调参批)
|
||||
@export var curio_quota_time_sand_rare: int = 2 # [0,99]——时沙(T4 投放 provisional 归调参批)
|
||||
@export var curio_quota_perfect_insurance_rare: int = 1 # [0,99]——完美保险(T4 投放 provisional 归调参批)
|
||||
|
||||
# —— 令牌件值(小/大数值对——v2.2 追认 b 定稿:20/50、+1/+2、+1/+2、+5/+10、恢复+6、AP+1)——
|
||||
@export var curio_value_attack_common: int = 1 # [0,999] knob——小增伤
|
||||
@export var curio_value_attack_rare: int = 2 # [0,999] knob——大增伤
|
||||
@export var curio_value_attack_excellent: int = 2 # [0,999] knob——大增伤
|
||||
@export var curio_value_max_hp_common: int = 5 # [0,999] knob——小生命
|
||||
@export var curio_value_max_hp_rare: int = 10 # [0,999] knob——大生命
|
||||
@export var curio_value_max_hp_excellent: int = 10 # [0,999] knob——大生命
|
||||
@export var curio_value_armor_common: int = 1 # [0,999] knob——小护甲
|
||||
@export var curio_value_armor_rare: int = 2 # [0,999] knob——大护甲
|
||||
@export var curio_value_gp_common: int = 20 # [0,999] knob——小GP
|
||||
@export var curio_value_gp_rare: int = 50 # [0,999] knob——大GP
|
||||
@export var curio_value_heal_rare: int = 6 # [0,999] knob——恢复(当即回血,封顶生效上限)
|
||||
@export var curio_value_refresh_rare: int = 1 # [0,9] knob(provisional)——刷新次数件兑付值(三选一重抽次数 +N——_attune_refresh_x 增量,不进加算区)
|
||||
@export var curio_value_ap_rarest: int = 1 # [0,10] knob——AP 件每场战斗首轮行动点基线增量
|
||||
@export var curio_value_armor_excellent: int = 2 # [0,999] knob——大护甲
|
||||
@export var curio_value_gp_common: int = 20 # [0,999] knob——小GP
|
||||
@export var curio_value_gp_excellent: int = 50 # [0,999] knob——大GP
|
||||
@export var curio_value_heal_excellent: int = 6 # [0,999] knob——恢复(当即回血,封顶生效上限)
|
||||
@export var curio_value_refresh_excellent: int = 1 # [0,9] knob(provisional)——刷新次数件兑付值(三选一重抽次数 +N——_attune_refresh_x 增量,不进加算区)
|
||||
@export var curio_value_ap_rare: int = 1 # [0,10] knob——AP 件每场战斗首轮行动点基线增量
|
||||
@export var curio_value_rap_rare: int = 3 # [0,99] knob(provisional)——储备金袋 RAP +N
|
||||
@export var curio_value_attune_battery_rare: int = 1 # [0,9] knob(provisional)——同调电池充能 +N
|
||||
@export var curio_value_time_sand_rare: int = 2 # [0,99] knob(provisional)——时沙 下轮 AP +N
|
||||
@export var curio_value_perfect_insurance_rare: int = 1 # [0,9] knob(provisional)——完美保险 资格保护次数
|
||||
|
||||
# —— 已拿上限 cap(迭代 20 T2 概率与上限解耦〔奇物.md 池结构案D〕——案②令牌池+提前退役:
|
||||
# 概率位=档+令牌数〔牌库物理张数〕、上限=本字段〔拿了才计、刷出不计〕;cap 满后该 kind 从候选
|
||||
# 视图剔除〔提前退役〕。99=实质无上限占位,实数归调参批;专长 cap 在 feats.tres 行字段〕)——
|
||||
@export var curio_cap_attack: int = 99 # [1,99]
|
||||
@export var curio_cap_max_hp: int = 99 # [1,99]
|
||||
@export var curio_cap_armor: int = 99 # [1,99]
|
||||
@export var curio_cap_gp: int = 99 # [1,99]
|
||||
@export var curio_cap_ap: int = 99 # [1,99]
|
||||
@export var curio_cap_heal: int = 99 # [1,99]
|
||||
@export var curio_cap_refresh: int = 99 # [1,99]
|
||||
@export var curio_cap_rap: int = 99 # [1,99]
|
||||
@export var curio_cap_attune_battery: int = 99 # [1,99]
|
||||
@export var curio_cap_time_sand: int = 99 # [1,99]
|
||||
@export var curio_cap_perfect_insurance: int = 99 # [1,99]
|
||||
|
||||
# —— 专长获取门票(迭代 15 卡② T5——非 Boss 渠道的专长入口形态,奇物.md v2.7 渠道轴定位;
|
||||
# 四口径契约④:拿到即开不可囤积/罕见只在概率不在价格/一局限购一次/计步保底连续 5 展示未出
|
||||
@@ -97,45 +127,83 @@ func curio_apply_route(kind: String) -> String:
|
||||
return String(CURIO_APPLY_ROUTES.get(kind, ""))
|
||||
|
||||
|
||||
## 令牌配额读数(未列 kind×tier 组合 = 0——heal 无普通档/ap 无普通稀有档按 v2.2 构成)
|
||||
## 令牌配额读数(未列 kind×tier 组合 = 0——迭代 20 T2 四级构成:属性四类 common/excellent、
|
||||
## heal/refresh excellent、ap rare、凭证四 kind rare 占位 0)
|
||||
func curio_quota(kind: String, tier: String) -> int:
|
||||
match [kind, tier]:
|
||||
["attack", TIER_COMMON]: return curio_quota_attack_common
|
||||
["attack", TIER_RARE]: return curio_quota_attack_rare
|
||||
["attack", TIER_EXCELLENT]: return curio_quota_attack_excellent
|
||||
["max_hp", TIER_COMMON]: return curio_quota_max_hp_common
|
||||
["max_hp", TIER_RARE]: return curio_quota_max_hp_rare
|
||||
["max_hp", TIER_EXCELLENT]: return curio_quota_max_hp_excellent
|
||||
["armor", TIER_COMMON]: return curio_quota_armor_common
|
||||
["armor", TIER_RARE]: return curio_quota_armor_rare
|
||||
["armor", TIER_EXCELLENT]: return curio_quota_armor_excellent
|
||||
["gp", TIER_COMMON]: return curio_quota_gp_common
|
||||
["gp", TIER_RARE]: return curio_quota_gp_rare
|
||||
["heal", TIER_RARE]: return curio_quota_heal_rare
|
||||
["refresh", TIER_RARE]: return curio_quota_refresh_rare
|
||||
["ap", TIER_RAREST]: return curio_quota_ap_rarest
|
||||
["gp", TIER_EXCELLENT]: return curio_quota_gp_excellent
|
||||
["heal", TIER_EXCELLENT]: return curio_quota_heal_excellent
|
||||
["refresh", TIER_EXCELLENT]: return curio_quota_refresh_excellent
|
||||
["ap", TIER_RARE]: return curio_quota_ap_rare
|
||||
["rap", TIER_RARE]: return curio_quota_rap_rare
|
||||
["attune_battery", TIER_RARE]: return curio_quota_attune_battery_rare
|
||||
["time_sand", TIER_RARE]: return curio_quota_time_sand_rare
|
||||
["perfect_insurance", TIER_RARE]: return curio_quota_perfect_insurance_rare
|
||||
return 0
|
||||
|
||||
|
||||
## 令牌件值(kind × tier;v2.2 数值对——小/大两档)
|
||||
## 令牌件值(kind × tier;四级构成随配额表)
|
||||
func curio_value(kind: String, tier: String = TIER_COMMON) -> int:
|
||||
match [kind, tier]:
|
||||
["attack", TIER_COMMON]: return curio_value_attack_common
|
||||
["attack", TIER_RARE]: return curio_value_attack_rare
|
||||
["attack", TIER_EXCELLENT]: return curio_value_attack_excellent
|
||||
["max_hp", TIER_COMMON]: return curio_value_max_hp_common
|
||||
["max_hp", TIER_RARE]: return curio_value_max_hp_rare
|
||||
["max_hp", TIER_EXCELLENT]: return curio_value_max_hp_excellent
|
||||
["armor", TIER_COMMON]: return curio_value_armor_common
|
||||
["armor", TIER_RARE]: return curio_value_armor_rare
|
||||
["armor", TIER_EXCELLENT]: return curio_value_armor_excellent
|
||||
["gp", TIER_COMMON]: return curio_value_gp_common
|
||||
["gp", TIER_RARE]: return curio_value_gp_rare
|
||||
["heal", TIER_RARE]: return curio_value_heal_rare
|
||||
["refresh", TIER_RARE]: return curio_value_refresh_rare
|
||||
["ap", TIER_RAREST]: return curio_value_ap_rarest
|
||||
["gp", TIER_EXCELLENT]: return curio_value_gp_excellent
|
||||
["heal", TIER_EXCELLENT]: return curio_value_heal_excellent
|
||||
["refresh", TIER_EXCELLENT]: return curio_value_refresh_excellent
|
||||
["ap", TIER_RARE]: return curio_value_ap_rare
|
||||
["rap", TIER_RARE]: return curio_value_rap_rare
|
||||
["attune_battery", TIER_RARE]: return curio_value_attune_battery_rare
|
||||
["time_sand", TIER_RARE]: return curio_value_time_sand_rare
|
||||
["perfect_insurance", TIER_RARE]: return curio_value_perfect_insurance_rare
|
||||
return 0
|
||||
|
||||
|
||||
## 档位判读序(MC 策略 316-f——1 级最稀有:rarest > rare > common;并列同档取最左由调用方稳定序保证)
|
||||
## 已拿上限读数(迭代 20 T2 案②——kind 维度;专长 cap 走 feats.tres 行字段不走本面)
|
||||
func curio_cap(kind: String) -> int:
|
||||
match kind:
|
||||
"attack": return curio_cap_attack
|
||||
"max_hp": return curio_cap_max_hp
|
||||
"armor": return curio_cap_armor
|
||||
"gp": return curio_cap_gp
|
||||
"ap": return curio_cap_ap
|
||||
"heal": return curio_cap_heal
|
||||
"refresh": return curio_cap_refresh
|
||||
"rap": return curio_cap_rap
|
||||
"attune_battery": return curio_cap_attune_battery
|
||||
"time_sand": return curio_cap_time_sand
|
||||
"perfect_insurance": return curio_cap_perfect_insurance
|
||||
return 99
|
||||
|
||||
|
||||
## Boss 专长三选一按档配比加权(迭代 20 T5——奇物.md v3.1 rarity 语义约束「候选按档配比或
|
||||
## 加权抽取」兑付面;权重 provisional 归调参批——普通打底/优秀期待/稀有稀缺/传说头奖形态)
|
||||
func boss_feat_weight(tier: String) -> int:
|
||||
match tier:
|
||||
TIER_COMMON: return 100
|
||||
TIER_EXCELLENT: return 45
|
||||
TIER_RARE: return 15
|
||||
TIER_LEGENDARY: return 5
|
||||
return 1
|
||||
|
||||
|
||||
## 档位判读序(MC 策略 316-f——1 级最稀有:legendary > rare > excellent > common;并列同档取最左由调用方稳定序保证)
|
||||
func tier_rank(tier: String) -> int:
|
||||
match tier:
|
||||
TIER_RAREST: return 3
|
||||
TIER_RARE: return 2
|
||||
TIER_LEGENDARY: return 4
|
||||
TIER_RARE: return 3
|
||||
TIER_EXCELLENT: return 2
|
||||
TIER_COMMON: return 1
|
||||
return 0
|
||||
|
||||
@@ -148,9 +216,12 @@ func tier_total(tier: String) -> int:
|
||||
return total
|
||||
|
||||
|
||||
## 牌库总量(结构锚——罕见档随道具件退役收窄后 98 张)
|
||||
## 牌库总量(结构锚——T2 四级 98 + T4 凭证投放 6 = 104:70+25+9+0)
|
||||
func deck_total() -> int:
|
||||
return tier_total(TIER_COMMON) + tier_total(TIER_RARE) + tier_total(TIER_RAREST)
|
||||
var total := 0
|
||||
for tier in TIERS:
|
||||
total += tier_total(tier)
|
||||
return total
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
@@ -168,10 +239,12 @@ func validate() -> Array:
|
||||
var v := curio_value(kind, tier)
|
||||
if v < 0 or v > 999:
|
||||
errs.append({"category": "domain", "message": "curio_value(%s,%s) 越域 [0,999]: %d" % [kind, tier, v]})
|
||||
if curio_cap(kind) < 1 or curio_cap(kind) > 99:
|
||||
errs.append({"category": "domain", "message": "curio_cap(%s) 越域 [1,99]: %d" % [kind, curio_cap(kind)]})
|
||||
for tier in TIERS:
|
||||
var total := tier_total(tier)
|
||||
if total != int(TIER_QUOTAS.get(tier, -1)):
|
||||
errs.append({"category": "domain", "message": "档位配额结构失锚(v2.2 追认 b——%s 应=%d 实=%d;改总量须重裁)" % [tier, int(TIER_QUOTAS.get(tier, -1)), total]})
|
||||
errs.append({"category": "domain", "message": "档位配额结构失锚(四级锚 70/25/3/0——%s 应=%d 实=%d;改总量须重裁)" % [tier, int(TIER_QUOTAS.get(tier, -1)), total]})
|
||||
if curio_candidate_count < 1 or curio_candidate_count > 6:
|
||||
errs.append({"category": "domain", "message": "curio_candidate_count 越域 [1,6]: %d" % curio_candidate_count})
|
||||
return errs
|
||||
@@ -1,8 +1,9 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 七核心流派各 ≥3 件入局内随机渠道后,标签+档位激活引擎能否机械判「成型」、
|
||||
# 专长带能否挂进令牌池稀有/罕见档随机投放(P0——迭代 12 双层 BD 实施批 T1)
|
||||
# Date: 2026-10-06(内容数据驱动扩展批 T3——行换代:hook/effects 挂点形态退役,triggers 事件订阅形态
|
||||
# 唯一合法行形——旧 23 挂点枚举消亡:事件名+效果类型取代,需求卡片 R4 裁决)
|
||||
# Validation Question: 专长 27 件订阅行能否经效果引擎统一解释执行、逐件行为与声明一致(P0——
|
||||
# 迭代 20 专长实装批 T3;前史:迭代 12 双层 BD 实施批 T1 建表/迭代 17 内容数据驱动扩展批 T3 行换代)
|
||||
# Date: 2026-10-11(迭代 20 专长实装批 T3——重设计换代:流派标签/档位激活机制退役〔奇物.md v1
|
||||
# 设计原则「不设流派标签、不设档位机制」〕;rarity 四级〔跨层框架〕;cap 已拿上限字段〔池结构案D
|
||||
# 概率与上限解耦——每件标注两个独立属性:概率位=档+rarity、上限=cap(拿了才计、刷出不计)〕)
|
||||
# 概念指路:本表的专长,与 adventure.tres 的属性值提升、资源,同属一个对象——
|
||||
# 「奇物」(一次冒险内各渠道给予玩家的增益总称)。定义见 design/gdd/systems/冒险循环/奇物.md。
|
||||
# 它们是它分类轴上的枚举(专长 / 属性值提升 / 资源),不是三套系统;本表承载「专长」(专长 Feat)的逐项定义。
|
||||
@@ -11,29 +12,25 @@
|
||||
# 目标渠道:专长走关底 Boss 击败后专属三选一(不新增节点类型),见奇物文档 2.1 节。)
|
||||
# 数据配置 FeatTable schema(奇物·专长定义表;行 = Dictionary 键 feat_id,Rule 4 豁免
|
||||
# 定义表——TreasureItemTable 先例)。
|
||||
# 行字段:feat_id 主键 / display_name / rarity ∈ {common, rare, rarest}(对齐令牌池三档命名——
|
||||
# 投放档映射: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 行订阅扫雷侧事件
|
||||
# (EffectEngine.MINE_EVENTS——加载校验,违反=红线①触碰面)。
|
||||
# 行字段:feat_id 主键 / display_name / rarity ∈ {common, excellent, rare, legendary}(四级稀有度,
|
||||
# 与令牌池档位词表同名词族——令牌档 = rarity 直映射)/ cap 已拿上限(正整数,缺省 1——「同一件
|
||||
# 最多拿 N 次」;概率位与上限解耦见奇物.md 池结构节)/ desc 效果说明(文案表值化——呈现层读行,
|
||||
# 零硬编码)/ triggers 订阅行数组(订阅哪个事件 on / 什么条件 when / 什么效果 do——效果引擎
|
||||
# EffectEngine 统一解释执行;四词表封闭校验=EffectEngine.validate_trigger_row 单一事实源)。
|
||||
# 🔴 层级纪律(需求卡片红线 2):专长只走局内随机渠道(Boss 三选一/凭证开启)——本表行永不入同调位
|
||||
# (treasure_items 表)/局外树(T8 节点表)。
|
||||
# 退役注记(V14 旧实现摘除):ARCHETYPES 流派标签/档位激活(FeatsEngine 档位质变)随重设计退役——
|
||||
# 事件族(开格/插旗/踩雷/节奏/战斗系)仅系设计侧分组工具不上数据行不上卡面(命名铁律)。
|
||||
class_name FeatTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary] 键 feat_id
|
||||
|
||||
## 流派标签词表(七核心——喂雷/淘金不在本批,R5 甲/缓议)
|
||||
const ARCHETYPES := ["magic", "pierce", "normal", "dig", "flag", "charge", "devour"]
|
||||
## 稀有度词表(四级——与令牌池 TIERS 同名词族;令牌档 = rarity 直映射)
|
||||
const RARITIES := ["common", "excellent", "rare", "legendary"]
|
||||
|
||||
## 稀有度词表(对齐令牌池三档;投放档映射 relic→rare / rarest→rarest)
|
||||
const RARITIES := ["common", "rare", "rarest"]
|
||||
|
||||
## 行键白名单(🔴 零裸 flat 同族护栏——键出域=加载致命;triggers/desc=T3 换代新形态)
|
||||
const ROW_KEYS := ["feat_id", "display_name", "rarity", "archetype", "triggers", "desc", "note"]
|
||||
## 行键白名单(🔴 零裸 flat 同族护栏——键出域=加载致命)
|
||||
const ROW_KEYS := ["feat_id", "display_name", "rarity", "cap", "triggers", "desc", "note"]
|
||||
|
||||
## trigger 行键(订阅行三字段:on/when?/do)
|
||||
const TRIGGER_KEYS := ["on", "when", "do"]
|
||||
@@ -42,10 +39,8 @@ const TRIGGER_KEYS := ["on", "when", "do"]
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if rows.is_empty():
|
||||
return errs # 空表合法态(T1 基建先行——逐流派填行)
|
||||
return errs # 空表合法态(T1 基建先行——逐件填行)
|
||||
var seen := {}
|
||||
# 流派扫雷覆盖聚合(🔴 红线①铁律——每流派 ≥1 行订阅扫雷侧事件)
|
||||
var arch_mine := {} # archetype → bool
|
||||
for row in rows:
|
||||
var rid: String = row.get("feat_id", "")
|
||||
if rid == "":
|
||||
@@ -59,30 +54,18 @@ func validate() -> Array:
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % rid})
|
||||
if not RARITIES.has(String(row.get("rarity", ""))):
|
||||
errs.append({"category": "enum", "message": "%s rarity 出域 %s" % [rid, str(RARITIES)]})
|
||||
var arch := String(row.get("archetype", ""))
|
||||
if not ARCHETYPES.has(arch):
|
||||
errs.append({"category": "enum", "message": "%s archetype 出域 %s" % [rid, str(ARCHETYPES)]})
|
||||
if row.has("cap") and int(row.get("cap", 1)) < 1:
|
||||
errs.append({"category": "domain", "message": "%s cap 出域 [1,∞): %d" % [rid, int(row.get("cap", 1))]})
|
||||
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
|
||||
else:
|
||||
EffectEngine.validate_trigger_row(rid, row, row.get("triggers", []) as Array, errs)
|
||||
for k in row.keys():
|
||||
if not ROW_KEYS.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s" % [rid, String(k)]})
|
||||
# 🔴 红线① 扫雷触发点铁律:**有行的流派**各 ≥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:
|
||||
|
||||
@@ -41,7 +41,7 @@ const DIMENSIONS := ["info", "risk", "action", "plan", "counter"]
|
||||
# 内容数据驱动扩展批 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",
|
||||
const EFFECT_KINDS := ["mine_reveal", "reward_preview", "ap_grant",
|
||||
"mine_hit_halve", "reflect_damage", "ap_rounds_front", "ap_charge_safe", "cell_removal",
|
||||
"reward_weight_bias", "bonus_refresh",
|
||||
"reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick"]
|
||||
@@ -49,7 +49,7 @@ const TARGETS := ["none", "area_3x3", "board_wide"]
|
||||
const USE_PHASES := ["active_dig"]
|
||||
const FORMS := ["", "passive", "active_charged"]
|
||||
# magnitude 消费面(≥1 校验):grant_ap 与同调 N 值 kind;其余 kind 活行 magnitude 恒 0
|
||||
const MAGNITUDE_KINDS := ["ap_grant", "venom_rider", "flag_burst", "reflect_damage", "ap_rounds_front",
|
||||
const MAGNITUDE_KINDS := ["ap_grant", "reflect_damage", "ap_rounds_front",
|
||||
"ap_charge_safe", "reward_weight_bias", "bonus_refresh", "grant_ap"]
|
||||
# 🔴 零裸 flat 结构护栏:行键白名单封闭(键出域=加载致命)——机制轴禁无条件属性加项的表结构承载
|
||||
const KEY_WHITELIST := ["item_id", "display_name", "dimension_tags", "effect_kinds", "target",
|
||||
|
||||
@@ -7,33 +7,37 @@ script = ExtResource("1")
|
||||
clear_heal_pct = 20
|
||||
curio_candidate_count = 3
|
||||
curio_quota_attack_common = 18
|
||||
curio_quota_attack_rare = 5
|
||||
curio_quota_attack_excellent = 5
|
||||
curio_quota_max_hp_common = 17
|
||||
curio_quota_max_hp_rare = 5
|
||||
curio_quota_max_hp_excellent = 5
|
||||
curio_quota_armor_common = 17
|
||||
curio_quota_armor_rare = 5
|
||||
curio_quota_armor_excellent = 5
|
||||
curio_quota_gp_common = 18
|
||||
curio_quota_gp_rare = 5
|
||||
curio_quota_heal_rare = 3
|
||||
curio_quota_refresh_rare = 2
|
||||
curio_quota_ap_rarest = 3
|
||||
curio_quota_gp_excellent = 5
|
||||
curio_quota_heal_excellent = 3
|
||||
curio_quota_refresh_excellent = 2
|
||||
curio_quota_ap_rare = 3
|
||||
curio_value_attack_common = 1
|
||||
curio_value_attack_rare = 2
|
||||
curio_value_attack_excellent = 2
|
||||
curio_value_max_hp_common = 5
|
||||
curio_value_max_hp_rare = 10
|
||||
curio_value_max_hp_excellent = 10
|
||||
curio_value_armor_common = 1
|
||||
curio_value_armor_rare = 2
|
||||
curio_value_armor_excellent = 2
|
||||
curio_value_gp_common = 20
|
||||
curio_value_gp_rare = 50
|
||||
curio_value_heal_rare = 6
|
||||
curio_value_refresh_rare = 1
|
||||
curio_value_ap_rarest = 1
|
||||
curio_value_gp_excellent = 50
|
||||
curio_value_heal_excellent = 6
|
||||
curio_value_refresh_excellent = 1
|
||||
curio_value_ap_rare = 1
|
||||
feat_ticket_chance = 20
|
||||
feat_ticket_quota = 2
|
||||
feat_ticket_limit = 1
|
||||
feat_ticket_pity_step = 5
|
||||
|
||||
; TP0 令牌池换代(2026-10-03,node-reward v2.2 追认 b 落地):三选一池 = 固定配额令牌池——
|
||||
; 迭代 20 T2 池结构案D 换代(2026-10-11,奇物.md 池结构与抽取 v1):四级档位 70/25/3/0 = 98 张
|
||||
; (普通 70/优秀 25/稀有 3/传说 0=头奖位留空——旧 70/25/3 直映射改名承袭零偏离已裁结构锚);
|
||||
; 概率与上限解耦(案②令牌池+提前退役):概率位=档+令牌数、已拿上限=curio_cap_*(拿了才计);
|
||||
; 曝光槽=三选一 2 格全池+1 格优秀档以上子池(子池空回退全池);凭证不进子池。
|
||||
; 前史 TP0 令牌池换代(2026-10-03,node-reward v2.2 追认 b 落地):三选一池 = 固定配额令牌池——
|
||||
; 普通 70/稀有 25/罕见 3 = 98 张(罕见档原列「道具件 2」随 treasure_item 池种退役收窄——台账自决披露项);
|
||||
; 洗牌不放回抽 3、选中踢池未选回池、池耗即枯竭;
|
||||
; 种类次数上限退役(池消耗即上限;货架库存同账本=牌库剩余令牌数)。
|
||||
|
||||
+847
-566
File diff suppressed because it is too large.
Load diff
@@ -73,59 +73,6 @@ rows = [{
|
||||
"slot_cost": 1,
|
||||
"charges_per_adventure": 2
|
||||
}, {
|
||||
"item_id": "venom_edge",
|
||||
"display_name": "淬毒刃纹",
|
||||
"dimension_tags": ["counter"],
|
||||
"desc": "克制正面直伤+2",
|
||||
"target": "none",
|
||||
"magnitude": 2,
|
||||
"use_phase": "active_dig",
|
||||
"form": "passive",
|
||||
"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"],
|
||||
"desc": "每标中一旗全体敌受2",
|
||||
"target": "none",
|
||||
"magnitude": 2,
|
||||
"use_phase": "active_dig",
|
||||
"form": "passive",
|
||||
"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"],
|
||||
|
||||
@@ -50,11 +50,14 @@ func _initialize() -> void:
|
||||
feat_mode = args[6]
|
||||
var feat_profile := {}
|
||||
if feat_mode == "formed":
|
||||
# 迭代 20 T8 画像换代——旧 14 件随老 27 件退役;新画像=五事件族覆盖 14 件
|
||||
# (开格 4/插旗 3/踩雷 4/节奏 2/战斗 1——每族含跨事件账本件,账本件=攒层/计数面 MC 读数承载)
|
||||
feat_profile = {"mode": "on", "seed_ids": [
|
||||
"arcane_ember", "mana_weaver", "pierce_shot", "chain_arrows",
|
||||
"miner_stubborn", "miner_hammer", "greedy_lantern", "vein_intuition",
|
||||
"mine_atlas", "nest_hunter", "armory_prep", "iron_wall",
|
||||
"ley_harvest", "thorn_weaver"]}
|
||||
"twin_pick", "geologist", "hard_hat", "frontier_miner",
|
||||
"seal_echo", "seal_smith", "seal_stake",
|
||||
"pain_memory", "trap_instinct", "chain_traps", "blood_honey",
|
||||
"last_drop", "market_close",
|
||||
"execution_scholar"]}
|
||||
elif feat_mode == "ab_a":
|
||||
feat_profile = {"mode": "off", "tree_rules": ["posture_first_strike"]}
|
||||
elif feat_mode == "ab_b":
|
||||
|
||||
@@ -277,7 +277,7 @@ static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> voi
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
# 三选一确定性选件(TP0 316-f——1 级优先度=最稀有〔档位最高 rarest>rare>common〕、2 级=最左;
|
||||
# 三选一确定性选件(TP0 316-f——1 级优先度=最稀有〔迭代 20 四级:legendary>rare>excellent>common〕、2 级=最左;
|
||||
# 候选并列同档取最左=稳定序保证。空候选/未知档位 = 回退最左防御口径。
|
||||
# 成型画像(迭代 12 T7——relic 模式 on:候选含 relic 令牌时优先在 relic 集内按 316-f 择一
|
||||
# 〔贪心捕捞→特性入局→成型画像全流派覆盖——R3 乙单锚承载〕;off=relic 与属性加成型特性同规则竞争)
|
||||
@@ -296,9 +296,11 @@ static func _pick_token(candidates: Array, feat_mode: String = "off") -> Diction
|
||||
var t: Dictionary = tok as Dictionary
|
||||
var rank := 0
|
||||
match String(t.get("tier", "")):
|
||||
"rarest":
|
||||
rank = 3
|
||||
"legendary":
|
||||
rank = 4
|
||||
"rare":
|
||||
rank = 3
|
||||
"excellent":
|
||||
rank = 2
|
||||
"common":
|
||||
rank = 1
|
||||
|
||||
+289
-223
@@ -218,15 +218,17 @@ func _smoke_adventure_table() -> void:
|
||||
"clear_heal_pct ∈ [0,100](%d——通关部分回血比例)" % int(_db.adventure.clear_heal_pct))
|
||||
for tier in _db.adventure.TIERS:
|
||||
check(int(_db.adventure.tier_total(tier)) == int(_db.adventure.TIER_QUOTAS[tier]),
|
||||
"档位配额结构锚(v2.2 追认 b):%s = %d 张" % [tier, int(_db.adventure.TIER_QUOTAS[tier])])
|
||||
check(int(_db.adventure.deck_total()) == 98, "令牌池总量 = 98 张(普通 70/稀有 25/罕见 3——罕见档随道具件池种退役收窄)")
|
||||
check(int(_db.adventure.curio_value("gp", "common")) == 20 and int(_db.adventure.curio_value("gp", "rare")) == 50
|
||||
and int(_db.adventure.curio_value("attack", "common")) == 1 and int(_db.adventure.curio_value("attack", "rare")) == 2
|
||||
and int(_db.adventure.curio_value("max_hp", "common")) == 5 and int(_db.adventure.curio_value("max_hp", "rare")) == 10
|
||||
and int(_db.adventure.curio_value("armor", "common")) == 1 and int(_db.adventure.curio_value("armor", "rare")) == 2,
|
||||
"数值对(v2.2):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2")
|
||||
check(int(_db.adventure.curio_value("heal", "rare")) == 6 and int(_db.adventure.curio_value("ap", "rarest")) == 1,
|
||||
"恢复件 +6 当即回血(新入池种类,rare 档)· AP +1(rarest 档)")
|
||||
"档位配额结构锚(四级 70/25/3/0 直映射承袭——迭代 20 T2):%s = %d 张" % [tier, int(_db.adventure.TIER_QUOTAS[tier])])
|
||||
check(int(_db.adventure.deck_total()) == 104, "令牌池总量 = 104 张(四级 普通 70/优秀 25/稀有 9/传说 0——T2 直映射 98+T4 凭证投放 6)〔专长批 V5〕")
|
||||
check(int(_db.adventure.curio_value("gp", "common")) == 20 and int(_db.adventure.curio_value("gp", "excellent")) == 50
|
||||
and int(_db.adventure.curio_value("attack", "common")) == 1 and int(_db.adventure.curio_value("attack", "excellent")) == 2
|
||||
and int(_db.adventure.curio_value("max_hp", "common")) == 5 and int(_db.adventure.curio_value("max_hp", "excellent")) == 10
|
||||
and int(_db.adventure.curio_value("armor", "common")) == 1 and int(_db.adventure.curio_value("armor", "excellent")) == 2,
|
||||
"数值对(四级换代承袭):小GP 20/大GP 50 · 小增伤 +1/大 +2 · 小生命 +5/大 +10 · 小护甲 +1/大 +2")
|
||||
check(int(_db.adventure.curio_value("heal", "excellent")) == 6 and int(_db.adventure.curio_value("ap", "rare")) == 1,
|
||||
"恢复件 +6 当即回血(优秀档承袭)· AP +1(稀有档承袭)〔专长批 V14 换代〕")
|
||||
check(int(_db.adventure.curio_cap("gp")) == 99 and int(_db.adventure.curio_cap("attack")) == 99,
|
||||
"cap 占位在域(案②提前退役——99=实质无上限,实数归调参批)〔专长批 V8 形态面〕")
|
||||
check(_db.attunement_growth != null and int(_db.attunement_slot_cap()) == 3
|
||||
and Attunement.slots(_db, null) == 0,
|
||||
"同调位读数源=attunement_growth 谓词表(slot_cap 3;未注入档求值 0——transitional_sync_slots 过渡键已退役,迭代 11 第①批 T7)")
|
||||
@@ -427,8 +429,8 @@ func _smoke_adventure() -> void:
|
||||
var k := 5.0 # hp+400 ⇒ 调试浮窗百分比后乘 ×5(加算先加、百分比后乘——生效次序口径)
|
||||
var exp_base := maxi(1, int(round(float(int(_db.hero.max_hp)) * k)))
|
||||
# 模拟档 v3.4 商店贪心购血(数值修复批)合法抬升——上限窗口 = 基础 + 加算区 3 件 + 购血余量;
|
||||
# TP0 数值对后 max_hp 件有大生命 +10(rare 档)——上界按大件最坏构成 3×10+购血 15=+45(TP0 放宽)
|
||||
var hp_ceiling := maxi(1, int(round(float(int(_db.hero.max_hp) + 3 * int(_db.adventure.curio_value("max_hp", "rare")) + 15) * k)))
|
||||
# 大生命 +10(迭代 20 T2 四级换代:优秀档)——上界按大件最坏构成 3×10+购血 15=+45(TP0 放宽承袭)
|
||||
var hp_ceiling := maxi(1, int(round(float(int(_db.hero.max_hp) + 3 * int(_db.adventure.curio_value("max_hp", "excellent")) + 15) * k)))
|
||||
var seq_ok := seq.size() > 0 and int(seq[0]) == exp_base
|
||||
for v in (seq as Array):
|
||||
if int(v) < exp_base or int(v) > hp_ceiling:
|
||||
@@ -631,58 +633,78 @@ func _smoke_adventure() -> void:
|
||||
"放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)")
|
||||
|
||||
|
||||
# ---- 双层 BD 规则改写型特性基建冒烟(迭代 12 T1——feat_table/档位激活引擎/规则改写型特性带池接线)----
|
||||
# ---- 专长重设计实装冒烟(迭代 20 T3——feats 四级换代 · 案D 池接线 · V3 订阅面逐件三列)----
|
||||
|
||||
func _smoke_feats() -> void:
|
||||
print("--- 规则改写型特性基建(feat_table · 档位激活引擎 · 规则改写型特性带接线)---")
|
||||
# ① 现行表牌库构成:七流派 27 件全量入池——98 数值令牌+27 relic 令牌(批次一铺量完成)
|
||||
check(_db.feats != null and (_db.feats.rows as Array).size() == 27, "feat_table 已加载(七流派 27 件全量——批次一 T2-T6 铺量完成)")
|
||||
print("--- 专长重设计实装(feat_table 四级 · 专长带池接线 · 订阅面逐件)---")
|
||||
# ① 现行表牌库构成:重设计 29 件全量入池——98 数值令牌+29 relic 令牌(四级 rarity 直映射)
|
||||
check(_db.feats != null and (_db.feats.rows as Array).size() == 29, "feat_table 已加载(重设计 29 件=基础 27+迁入 2——卡文 §10.4 推荐案口径)")
|
||||
check(_db.feats.validate().is_empty(), "feat_table.validate 零违例(四级 rarity/cap 域/trigger 词表/行键白名单全过)")
|
||||
var a_r0 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(71))
|
||||
check(a_r0.start_adventure(71, {"first_script": true}), "规则改写型特性带场景:冒险启动(seed 71)")
|
||||
check(a_r0.start_adventure(71, {"first_script": true}), "专长带场景:冒险启动(seed 71)")
|
||||
var feat_tokens := 0
|
||||
var rarest_tokens := 0
|
||||
var tier_counts := {}
|
||||
for t in a_r0._curio_deck:
|
||||
if String((t as Dictionary).get("kind", "")) == "relic":
|
||||
feat_tokens += 1
|
||||
if String((t as Dictionary).get("tier", "")) == "rarest":
|
||||
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 魔能共鸣 dig_reveal 订阅在案)")
|
||||
# ② 档位激活引擎(纯函数直测——构造表 4 件 magic + 1 件 flag;内容数据驱动扩展批 T3 断言面随迁:
|
||||
# 行=triggers 事件订阅形态,挂点投影断言换代=效果引擎订阅装载序〔feats 表序〕)
|
||||
var tk := String((t as Dictionary).get("tier", ""))
|
||||
tier_counts[tk] = int(tier_counts.get(tk, 0)) + 1
|
||||
check(feat_tokens == 29 and int(tier_counts.get("common", 0)) == 12 and int(tier_counts.get("excellent", 0)) == 9 and int(tier_counts.get("rare", 0)) == 8 and a_r0._curio_deck.size() == 133,
|
||||
"专长带入池:29 件四级直映射(普通 12/优秀 9/稀有 8/传说 0)——牌库 104+29=133〔专长批 V2〕")
|
||||
# ② 旧 27 件退役钉面(V2——arcane_ember 系旧件表首行;全仓 grep 零残留归批末静态核查)
|
||||
check(_db.feats.row_by_id("arcane_ember").is_empty() and _db.feats.row_by_id("magma_body").is_empty(),
|
||||
"旧 27 件已退役:arcane_ember/magma_body 表内零残留〔专长批 V2〕")
|
||||
check(_db.treasure_items.row_by_id("venom_edge").is_empty() and _db.treasure_items.row_by_id("flag_burst").is_empty(),
|
||||
"迁出收尾:treasure_items 无 venom_edge/flag_burst 行(同调目录自然收窄)〔专长批 V13〕")
|
||||
# ③ 契约面静态钉扎(V18/V19/V21/V22——人工核对 V20/V21 裁决面以外的可机检半)
|
||||
var kinds_ok := true
|
||||
for t in a_r0._curio_deck:
|
||||
var kk := String((t as Dictionary).get("kind", ""))
|
||||
if kk != "relic" and not _db.adventure.CURIO_KINDS.has(kk):
|
||||
kinds_ok = false
|
||||
check(kinds_ok, "层级纪律:牌库 kind 全集=CURIO_KINDS+relic(秘宝 item_id 零入池)〔专长批 V18〕")
|
||||
var rewrite_rows := 0
|
||||
for row_v in (_db.feats.rows as Array):
|
||||
for tv in ((row_v as Dictionary).get("triggers", []) as Array):
|
||||
if String(((tv as Dictionary).get("do", {}) as Dictionary).get("kind", "")) == "rewrite_board":
|
||||
rewrite_rows += 1
|
||||
check(rewrite_rows == 0, "棋盘事实不可变:新 29 行零棋盘改写类订阅(rewrite_board 已除名消费面)〔专长批 V19〕")
|
||||
var bad_words := 0
|
||||
for row_v in (_db.feats.rows as Array):
|
||||
var r_d: Dictionary = row_v as Dictionary
|
||||
for txt_v in [String(r_d.get("display_name", "")), String(r_d.get("desc", ""))]:
|
||||
if String(txt_v).contains("雷") or String(txt_v).contains("插旗"):
|
||||
bad_words += 1
|
||||
check(bad_words == 0, "对外命名纪律:29 行 display_name/desc 零「雷/插旗」字面(封印/陷阱风味系)〔专长批 V21 初筛〕")
|
||||
check(float((_db.feats.row_by_id("trap_burst").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("mult", 0)) > 1.0
|
||||
and int((_db.feats.row_by_id("blood_honey").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("value", 99)) < 5
|
||||
and int((_db.feats.row_by_id("stone_skin").get("triggers", [{}])[0] as Dictionary).get("do", {}).get("cap", 0)) == 3,
|
||||
"数值纪律:陷阱爆发倍率>1 · 血蜜回复<典型陷阱伤 · 石肤上限 3 在册(结构锚形态内 provisional)〔专长批 V22 机检半〕")
|
||||
# ④ 临时表注入(新 schema 形态——零 archetype/cap 直用):5 行 relic 入牌库/兑付/踢池/断点往返
|
||||
var rt := FeatTable.new()
|
||||
rt.rows = [
|
||||
{"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}}]},
|
||||
{"feat_id": "x1", "display_name": "试一", "rarity": "common", "desc": "测试揭示", "cap": 1,
|
||||
"triggers": [{"on": "mine_hit", "do": {"kind": "reveal_tiles", "value": 1}}]},
|
||||
{"feat_id": "x2", "display_name": "试二", "rarity": "excellent", "desc": "测试回复", "cap": 1,
|
||||
"triggers": [{"on": "mine_hit", "do": {"kind": "heal", "value": 2}}]},
|
||||
{"feat_id": "x3", "display_name": "试三", "rarity": "rare", "desc": "测试群伤", "cap": 1,
|
||||
"triggers": [{"on": "flag_hit", "do": {"kind": "open_ticket", "channel": "blast", "value": 3}}]},
|
||||
{"feat_id": "x4", "display_name": "试四", "rarity": "rare", "desc": "测试减伤", "cap": 2,
|
||||
"triggers": [{"on": "damage_taken", "when": [{"fact": "hp_ratio", "op": "lte", "value": 50}], "do": {"kind": "damage_reduce", "value": 2}}]},
|
||||
{"feat_id": "x5", "display_name": "试五", "rarity": "legendary", "desc": "测试AC", "cap": 1,
|
||||
"triggers": [{"on": "flag_hit", "do": {"kind": "ac_bonus", "value": 1, "mode": "round"}}]},
|
||||
]
|
||||
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 档二——以撒套装/小骨铭文先例)")
|
||||
check(FeatsEngine.is_formed(tiers, "magic") and not FeatsEngine.is_formed(tiers, "flag"),
|
||||
"成型判读:magic 成型(tier≥1)· flag 未成型(散件单体保底——档位只质变不启动)")
|
||||
var t2 := FeatsEngine.tiers_for(["m1", "m2"], rt)
|
||||
check(int(t2.get("magic", -1)) == 1, "档位激活:2 件→档一(同集合同档位确定性——MC 逐位可复放承载)")
|
||||
# ③ 规则改写型特性带接线(临时表注入——relic 令牌入牌库/兑付入持有集/踢池/断点往返;测试尾还原现行表)
|
||||
check(rt.validate().is_empty(), "新 schema 构造表 validate 零违例(四级 rarity/cap 字段/词表全过——流派标签已退役零 archetype)")
|
||||
var live_rows: FeatTable = _db.feats
|
||||
_db.feats = rt
|
||||
var a_r = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(72))
|
||||
check(a_r.start_adventure(72, {"first_script": true}), "规则改写型特性带场景:冒险启动(seed 72——注入 5 行临时表)")
|
||||
check(a_r.start_adventure(72, {"first_script": true}), "专长带场景:冒险启动(seed 72——注入 5 行临时表)")
|
||||
var deck_relic := 0
|
||||
for t in a_r._curio_deck:
|
||||
if String((t as Dictionary).get("kind", "")) == "relic":
|
||||
deck_relic += 1
|
||||
check(deck_relic == 5 and a_r._curio_deck.size() == 103,
|
||||
"规则改写型特性带入池:5 行 relic 每件 1 张(common/rare→稀有档 rarest→罕见档)——牌库 98+5=103")
|
||||
check(deck_relic == 5 and a_r._curio_deck.size() == 109,
|
||||
"专长带入池:5 行 relic 每件 1 张(四级 rarity 直映射)——牌库 104+5=109")
|
||||
# T4 渠道白名单断言(迭代 15 卡②——渠道轴定位:通关渠道只出专长/节点渠道零专长)
|
||||
var feat_seen: Dictionary = {}
|
||||
var node_relic_seen := false
|
||||
@@ -694,15 +716,15 @@ func _smoke_feats() -> void:
|
||||
if String((t as Dictionary).get("kind", "")) == "relic":
|
||||
node_relic_seen = true
|
||||
check(feat_seen.size() >= 2, "通关渠道只出专长(60 次重抽 ≥2 种不同 relic——身份键 feat_id 同档可多件)")
|
||||
check(not node_relic_seen, "节点渠道零专长(60 次重抽零 relic——专长在非 Boss 渠道只以门票形态出现,T4 渠道白名单)")
|
||||
# T5 门票行为断言(A5 拿到即开 / A6 保底计数——契约④四口径;独立 runner 防污染 a_r 牌库——后续 relic 兑付断言按 5 枚锚定)
|
||||
check(not node_relic_seen, "节点渠道零专长(60 次重抽零 relic——专长在非 Boss 渠道只以门票形态出现)")
|
||||
# T5 门票行为断言(A5 拿到即开 / A6 保底计数——契约④四口径;独立 runner 防污染 a_r 牌库)
|
||||
var a_t = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(91))
|
||||
a_t.start_adventure(91, {"first_script": true})
|
||||
var stock_before := {}
|
||||
for kk in ["attack", "gp", "heal"]:
|
||||
stock_before[kk] = a_t.pool_stock(kk)
|
||||
a_t._node_curio_pending = {"candidates": [{"kind": "feat_ticket", "tier": "rare"}, {"kind": "attack", "tier": "common"}], "aux_detail": []}
|
||||
check(a_t.apply_node_curio("feat_ticket", "rare"), "A5 门票兑付受理(节点渠道——拿到即开)")
|
||||
a_t._node_curio_pending = {"candidates": [{"kind": "feat_ticket", "tier": "excellent"}, {"kind": "attack", "tier": "common"}], "aux_detail": []}
|
||||
check(a_t.apply_node_curio("feat_ticket", "excellent"), "A5 门票兑付受理(节点渠道——拿到即开)")
|
||||
var tkt_cands: Array = (a_t.node_curio_pending_state().get("candidates", []) as Array)
|
||||
var tkt_all_relic := tkt_cands.size() > 0
|
||||
for t in tkt_cands:
|
||||
@@ -732,12 +754,8 @@ func _smoke_feats() -> void:
|
||||
offered = true
|
||||
break
|
||||
check(offered, "A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)")
|
||||
a_r._curio_pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "m3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||||
var deck_m3 := 0
|
||||
for t in a_r._curio_deck:
|
||||
if String((t as Dictionary).get("feat_id", "")) == "m3":
|
||||
deck_m3 += 1
|
||||
check(a_r.pick_curio("relic", "rare", "m3") and (a_r.feats_held() as Array).has("m3") and a_r.pool_stock("relic") == deck_relic - 1,
|
||||
a_r._curio_pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||||
check(a_r.pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.pool_stock("relic") == deck_relic - 1,
|
||||
"relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" % [deck_relic, deck_relic - 1])
|
||||
# 断点往返(持有集+余库 feat_id 随行)
|
||||
var sm_r = load("res://core/save_manager.gd").new("user://_smoke_feats_bp.json")
|
||||
@@ -745,176 +763,224 @@ func _smoke_feats() -> void:
|
||||
a_r.set_save_manager(sm_r)
|
||||
a_r._on_level_finished({"result": "victory", "hp": int(a_r.ledger.get("hero_hp", 26)), "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
var bp_r: Dictionary = sm_r.breakpoint_snapshot()
|
||||
check((bp_r.get("relics", []) as Array).has("m3"), "断点附随件:relics 持有集入快照(迭代 12 T1 扩列)")
|
||||
var bp_deck_m3 := 0
|
||||
check((bp_r.get("relics", []) as Array).has("x3"), "断点附随件:relics 持有集入快照(迭代 12 T1 扩列)")
|
||||
check(int((bp_r.get("adventure_counters", {}) as Dictionary).size()) >= 0, "断点附随件:adventure 计数器账本键在案(T6/T7 扩列)")
|
||||
var bp_deck_x3 := 0
|
||||
for t in (bp_r.get("bonus_deck", []) as Array):
|
||||
if String((t as Dictionary).get("feat_id", "")) == "m3":
|
||||
bp_deck_m3 += 1
|
||||
check(bp_deck_m3 == 0, "断点余库:已兑付 relic 令牌逐枚还原(m3 零残留——续玩不重复获得)")
|
||||
if String((t as Dictionary).get("feat_id", "")) == "x3":
|
||||
bp_deck_x3 += 1
|
||||
check(bp_deck_x3 == 0, "断点余库:已兑付 relic 令牌逐枚还原(x3 零残留——续玩不重复获得)")
|
||||
sm_r.clear_breakpoint()
|
||||
_db.feats = live_rows # 还原现行表(共享 _db 防污染——后续冒烟段零感知)
|
||||
# ④ 魔法流订阅面(内容数据驱动扩展批 T3 断言面随迁——_feat_flags_for 挂点求值退役:
|
||||
# 求值断言=效果引擎 rebuild+dispatch 命中面;落地行为=发射点施加面(本段下方各事件直测),
|
||||
# 全链分岔读数归 T6 MC 对拍)
|
||||
# ⑤ V3 订阅面逐件(附表 A 三列——命中面=EffectEngine dispatch 直测;账本面=counter 直读;
|
||||
# 落地行为=发射点施加面〔T1 施加段承载——本段按声明逐件钉命中与边界/反例〕)
|
||||
var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(73))
|
||||
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"]
|
||||
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 判档二确定性)")
|
||||
# ⑦ 开格/蓄力流订阅面(追加七件)
|
||||
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([])
|
||||
check(not panel._feat_section.visible, "特性段空态隐藏(视图空态纪律同特性段)")
|
||||
panel.set_feat_holdings(["魔法 ×2 · 档一", "开格 ×1 · 散件"])
|
||||
check(panel._feat_section.visible and panel._feat_list.get_child_count() == 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)
|
||||
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 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 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._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 fx = orch._fx
|
||||
# counter 落账 helper(dispatch 只返回命中不落账——施加面同构:counter_add 逐条落账本 helper 承载)
|
||||
var apply_fx := func(hits: Array) -> void:
|
||||
for h in hits:
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
var sv_c = h.get("set", null)
|
||||
fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if sv_c == null else int(sv_c), sv_c != null)
|
||||
# 开格系
|
||||
fx.rebuild(["twin_pick"], [], [])
|
||||
for i in 4:
|
||||
var hits_dp: Array = fx.dispatch("dig_paid", {"ap_cost": 1})
|
||||
var refund := false
|
||||
for h in hits_dp:
|
||||
if String(h.get("kind", "")) == "grant_ap":
|
||||
refund = true
|
||||
fx.counter_add("twin_digs", 1)
|
||||
if i == 1 or i == 3:
|
||||
check(refund, "双管镐:第 %d 次付费挖格返 1 AP(每第 2 次——mod_eq 预评估口径)" % (i + 1))
|
||||
else:
|
||||
check(not refund, "双管镐反例:第 %d 次不返(未满 2 次)" % (i + 1))
|
||||
fx.rebuild(["geologist"], [], [])
|
||||
var geo_hits: Array = fx.dispatch("dig_reveal", {"new_resource_type": true})
|
||||
apply_fx.call(geo_hits)
|
||||
check(orch._hits_of(geo_hits, "counter_add").size() == 1, "地质学家:首开新类型集 1 点〔专长批 V3〕")
|
||||
fx.counter_add("geo_types", 2) # 手动基数补齐(row1 已落账 1+2=3——满 4 种的预评估现值)
|
||||
var geo_hits2: Array = fx.dispatch("dig_reveal", {"new_resource_type": true})
|
||||
apply_fx.call(geo_hits2)
|
||||
check(orch._hits_of(geo_hits2, "reveal_tiles").size() == 1 and fx.counter("geo_types") == 0,
|
||||
"地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕")
|
||||
var geo_hits3: Array = fx.dispatch("dig_reveal", {"new_resource_type": false})
|
||||
check(orch._hits_of(geo_hits3, "counter_add").is_empty(), "地质学家反例:同类型重复首开不叠点〔专长批 V3〕")
|
||||
fx.rebuild(["hard_hat"], [], [])
|
||||
apply_fx.call(fx.dispatch("dig_reveal", {}))
|
||||
apply_fx.call(fx.dispatch("dig_reveal", {}))
|
||||
check(fx.counter("helm_layers") == 2, "安全帽:开安全格攒层 ×2〔专长批 V3〕")
|
||||
var hh_hits: Array = fx.dispatch("mine_hit", {"damage": 6})
|
||||
var hh_reduce := 0
|
||||
for h in hh_hits:
|
||||
if String(h.get("kind", "")) == "damage_reduce":
|
||||
hh_reduce = fx.counter(String(h.get("value_counter", "")))
|
||||
check(hh_reduce == 2, "安全帽:踩雷耗层减伤=层值 2(一次清零随施加面 set 行)〔专长批 V3〕")
|
||||
fx.rebuild(["skip_mining"], [], [])
|
||||
var sk_zero: Array = fx.dispatch("dig_reveal", {"is_blank_zero": true})
|
||||
apply_fx.call(sk_zero)
|
||||
check(orch._hits_of(sk_zero, "counter_add").size() == 1, "跳格采掘:空白 0 格攒 1 点〔专长批 V3〕")
|
||||
fx.counter_add("skip_pts", 1) # 手动基数补齐(已落账 1+1=2——满 3 点的预评估现值)
|
||||
var sk_full: Array = fx.dispatch("dig_reveal", {"is_blank_zero": true})
|
||||
apply_fx.call(sk_full)
|
||||
check(orch._hits_of(sk_full, "reveal_tiles").size() == 1,
|
||||
"跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕")
|
||||
check(orch._hits_of(fx.dispatch("dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty(),
|
||||
"跳格采掘反例:普通开格不计点〔专长批 V3〕")
|
||||
fx.rebuild(["blast_accounting"], [], [])
|
||||
for i in 5:
|
||||
var ba_hits: Array = fx.dispatch("dig_paid", {"ap_cost": 1})
|
||||
for h in ba_hits:
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx.counter_add(String(h.get("counter_id", "")), 1)
|
||||
check(orch._hits_of(fx.dispatch("round_end", {}), "grant_gp").size() == 1, "爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕")
|
||||
fx.rebuild(["frontier_miner"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1
|
||||
and orch._hits_of(fx.dispatch("dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty(),
|
||||
"边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕")
|
||||
# 插旗系
|
||||
fx.rebuild(["seal_echo"], [], [])
|
||||
apply_fx.call(fx.dispatch("flag_hit", {}))
|
||||
apply_fx.call(fx.dispatch("flag_hit", {}))
|
||||
var se_hits: Array = fx.dispatch("round_end", {})
|
||||
check(orch._hits_of(se_hits, "grant_ap").size() == 1 and String((orch._hits_of(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue",
|
||||
"封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕")
|
||||
fx.rebuild(["seal_smith"], [], [])
|
||||
var sm_remove := 0
|
||||
for i in 3:
|
||||
var sm_dispatch: Array = fx.dispatch("flag_hit", {})
|
||||
apply_fx.call(sm_dispatch)
|
||||
for h in sm_dispatch:
|
||||
if String(h.get("kind", "")) == "remove_mines":
|
||||
sm_remove += 1
|
||||
check(sm_remove == 1, "封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕")
|
||||
fx.rebuild(["seal_stake"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("flag_hit", {}), "ac_bonus").size() == 1
|
||||
and String((orch._hits_of(fx.dispatch("flag_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round",
|
||||
"封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕")
|
||||
fx.rebuild(["warning_beacon"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("flag_hit", {}), "preview_area").size() == 1, "示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕")
|
||||
# 踩雷/受伤系
|
||||
fx.rebuild(["pain_memory"], [], [])
|
||||
apply_fx.call(fx.dispatch("mine_hit", {}))
|
||||
apply_fx.call(fx.dispatch("mine_hit", {}))
|
||||
var pm_hits: Array = fx.dispatch("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
|
||||
var pm_bonus := 0
|
||||
for h in orch._hits_of(pm_hits, "attack_bonus"):
|
||||
pm_bonus = fx.counter(String(h.get("value_counter", "")))
|
||||
check(pm_bonus == 2, "疼痛记忆:攻击附加层值伤害=层值 2·用后清零(set 行随施加面)〔专长批 V3〕")
|
||||
fx.rebuild(["fuse_wire"], [], [])
|
||||
var fw_hits: Array = fx.dispatch("damage_taken", {"kind": "terrain", "amount": 6})
|
||||
var fw_shield := 0
|
||||
for h in fw_hits:
|
||||
if String(h.get("kind", "")) == "shield":
|
||||
fw_shield += int(h.get("value", 0))
|
||||
check(orch._hits_of(fx.dispatch("damage_taken", {"kind": "terrain", "amount": 3}), "shield").is_empty(),
|
||||
"保险丝反例:单次 <5 不攒点不触发〔专长批 V3〕")
|
||||
for h in fx.dispatch("damage_taken", {"kind": "terrain", "amount": 6}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
check(fw_shield == 0, "保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕")
|
||||
fx.rebuild(["blood_insight"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("mine_hit", {}), "reveal_tiles").size() == 1, "见血开窍:踩雷揭示 1 格〔专长批 V3〕")
|
||||
fx.rebuild(["trap_instinct"], [], [])
|
||||
apply_fx.call(fx.dispatch("mine_hit", {}))
|
||||
var ti_hits: Array = fx.dispatch("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
|
||||
var ti_val := 0
|
||||
for h in orch._hits_of(ti_hits, "attack_bonus"):
|
||||
ti_val = fx.counter(String(h.get("value_counter", "")))
|
||||
check(ti_val == 2, "陷阱本能:踩雷本轮攻击 +2(attack_bonus 通道——面板加算)〔专长批 V3〕")
|
||||
fx.rebuild(["chain_traps", "pain_memory"], [], [])
|
||||
apply_fx.call(fx.dispatch("mine_hit", {}))
|
||||
var ct_before: int = fx.counter("pain_layers")
|
||||
apply_fx.call(fx.dispatch("mine_hit", {}))
|
||||
check(fx.counter("pain_layers") == ct_before + 2, "连环陷阱:同轮第 2 颗起每颗额外 +1 层(疼痛记忆同账)〔专长批 V3〕")
|
||||
fx.rebuild(["trap_burst"], [], [])
|
||||
var tb_hits: Array = fx.dispatch("mine_hit", {"damage": 8})
|
||||
check(orch._hits_of(tb_hits, "blast_scaled").size() == 1
|
||||
and float((orch._hits_of(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0,
|
||||
"陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕")
|
||||
fx.rebuild(["blood_honey"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("mine_hit", {}), "heal").size() == 1, "血蜜:踩雷回复 N 生命〔专长批 V3〕")
|
||||
fx.rebuild(["stone_skin"], [], [])
|
||||
var ss_hits: Array = fx.dispatch("damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1})
|
||||
check(orch._hits_of(ss_hits, "ac_bonus").size() == 1
|
||||
and int((orch._hits_of(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3,
|
||||
"石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕")
|
||||
fx.rebuild(["last_stand"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1
|
||||
and orch._hits_of(fx.dispatch("damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty(),
|
||||
"背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕")
|
||||
# 跨族桥
|
||||
fx.rebuild(["breaker_strike"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("mine_hit", {"dug_under_flag": true}), "counter_add").size() == 1
|
||||
and orch._hits_of(fx.dispatch("mine_hit", {"dug_under_flag": false}), "counter_add").is_empty(),
|
||||
"破封一击:挖开带封印危险格 +1 层·无旗反例零触发〔专长批 V3〕")
|
||||
# 节奏系
|
||||
fx.rebuild(["map_exchange"], [], [])
|
||||
var me_hits: Array = fx.dispatch("dig_stop", {"ap_left": 4})
|
||||
apply_fx.call(me_hits)
|
||||
fx.counter_add("stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账
|
||||
check(orch._hits_of(me_hits, "counter_add").size() == 1, "弃图兑换:停手剩余 AP≥3 入账〔专长批 V3〕")
|
||||
var me_atk := 0
|
||||
for h in orch._hits_of(fx.dispatch("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}), "attack_bonus"):
|
||||
me_atk = fx.counter(String(h.get("value_counter", "")))
|
||||
check(me_atk == 4, "弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕")
|
||||
check(orch._hits_of(fx.dispatch("dig_stop", {"ap_left": 2}), "counter_add").is_empty(),
|
||||
"弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕")
|
||||
fx.rebuild(["last_drop"], [], [])
|
||||
var ld_hits: Array = fx.dispatch("round_start", {"round": 2, "ap_exhausted_last": true})
|
||||
check(orch._hits_of(ld_hits, "grant_ap").size() == 1, "一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕")
|
||||
check(orch._hits_of(fx.dispatch("round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty(),
|
||||
"一滴不剩反例:上轮有余不发〔专长批 V3〕")
|
||||
fx.rebuild(["market_close"], [], [])
|
||||
for i in 8:
|
||||
for h in fx.dispatch("dig_reveal", {}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx.counter_add(String(h.get("counter_id", "")), 1)
|
||||
check(orch._hits_of(fx.dispatch("round_end", {}), "grant_gp").size() == 1, "收盘:本轮开格≥8→+1 GP〔专长批 V3〕")
|
||||
# 战斗系
|
||||
fx.rebuild(["execution_scholar"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1,
|
||||
"斩首学者:击杀揭示 1 格〔专长批 V3〕")
|
||||
fx.rebuild(["executioner"], [], [])
|
||||
var ex_hits: Array = fx.dispatch("enemy_hp_threshold", {"unit": "e1", "pct": 30, "is_boss": false})
|
||||
check(orch._hits_of(ex_hits, "target_attack_bonus").size() == 1
|
||||
and orch._hits_of(fx.dispatch("enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty(),
|
||||
"处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕")
|
||||
fx.rebuild(["field_dressing"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("battle_won", {"round": 2}), "heal").size() == 1,
|
||||
"战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕")
|
||||
# 契约尖点
|
||||
fx.rebuild(["blood_tax"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("mine_hit", {}), "bonus_delta").size() == 1
|
||||
and String((orch._hits_of(fx.dispatch("flag_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk",
|
||||
"血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕")
|
||||
# 迁入 2 件
|
||||
fx.rebuild(["venom_edge"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1
|
||||
and orch._hits_of(fx.dispatch("clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty(),
|
||||
"淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕")
|
||||
fx.rebuild(["flag_burst"], [], [])
|
||||
check(orch._hits_of(fx.dispatch("flag_hit", {}), "open_ticket").size() == 1,
|
||||
"封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕")
|
||||
# 零持有
|
||||
var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74))
|
||||
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(),
|
||||
and orch0._fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": true, "terrain_kind": []}).is_empty()
|
||||
and orch0._fx_emit("mine_hit", {"damage": 5}).is_empty()
|
||||
and orch0._fx_emit("battle_won", {"round": 1}).is_empty(),
|
||||
"零持有订阅=零命中(relic 关着零扰动——伤害管线既有行为逐位不变)")
|
||||
# ⑦c 特性持有呈现段(2026-10-04 用户反馈补齐——C 区「特性」节;呈现换代面=06 在册不随批)
|
||||
var panel = load("res://presentation/views/hero_info_panel_view.gd").new()
|
||||
panel.set_feat_holdings([])
|
||||
check(not panel._feat_section.visible, "特性段空态隐藏(视图空态纪律同特性段)")
|
||||
panel.set_feat_holdings(["疼痛记忆 ×1", "双管镐 ×1"])
|
||||
check(panel._feat_section.visible and panel._feat_list.get_child_count() == 2,
|
||||
"特性段持有呈现(逐件行常驻可读——「局内奖励选了什么看不到」缺口承载)")
|
||||
check(String(load("res://presentation/battle_narrator.gd").feat_card_text({"feat_id": "pain_memory", "display_name": "疼痛记忆", "desc": "触陷阱攒 1 层"})).contains("触陷阱攒 1 层"),
|
||||
"特性卡文案表值化:desc 行字段直读(零硬编码映射)")
|
||||
|
||||
|
||||
# 手工场景驱动:L1 全程 + L2 推进到商店房(_drive_level 复用 + 兑付确认/选路/大奖励确认手工编排)
|
||||
@@ -2914,7 +2980,7 @@ func _smoke_settlement_segments() -> void:
|
||||
var rw: Dictionary = rows_r[i]
|
||||
var sc := String(rw.get("source_class", ""))
|
||||
var is_board_acct: bool = (String(rw.get("type", "")) == "damage" \
|
||||
and not ["hero_attack", "enemy_action", "feat_chain_arrows"].has(sc)) \
|
||||
and not ["hero_attack", "enemy_action", "fx_streak_segments"].has(sc)) \
|
||||
or String(rw.get("forfeit", "")) == "hero_down"
|
||||
if not is_board_acct:
|
||||
continue
|
||||
@@ -4643,7 +4709,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
var mp_a = load("res://core/meta_progression.gd").new(_db, sv_a)
|
||||
check(Attunement.slots(_db, sv_a) == 0 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 0,
|
||||
"白板档同调位求值 = 0(0 起步裁决)且 Y=0(未购无刷新能力——P1 甲初始 0 同构)")
|
||||
check(mp_a.toggle_attunement("venom_edge") == false,
|
||||
check(mp_a.toggle_attunement("nest_counter") == false,
|
||||
"满位禁选(Slots 0 时装配拒绝)")
|
||||
sv_a.add_keystone(200)
|
||||
check(mp_a.buy_attune_refresh() and sv_a.attune_refresh_level() == 1
|
||||
@@ -4653,9 +4719,9 @@ func _meta_settle_and_workshop() -> void:
|
||||
"stopped_at": 2, "keystone_total": 0, "master_seed": 21})
|
||||
check(Attunement.slots(_db, sv_a) == 1 and sv_a.boss_kill_count() == 1,
|
||||
"通关 1 关 → m1 达成 Slots=1 且 Boss 击杀总数=1(计数源收束入账)")
|
||||
check(mp_a.toggle_attunement("venom_edge") and mp_a.toggle_attunement("flag_burst") == false,
|
||||
check(mp_a.toggle_attunement("nest_counter") and mp_a.toggle_attunement("dawn_order") == false,
|
||||
"装配持久写入 + 满位禁选(Slots 1 装第二件拒绝)")
|
||||
check(mp_a.toggle_attunement("venom_edge") and mp_a.toggle_attunement("flag_burst"),
|
||||
check(mp_a.toggle_attunement("nest_counter") and mp_a.toggle_attunement("dawn_order"),
|
||||
"卸下恒可再装配(换装语义——装配序数组承载)")
|
||||
var sv_a2 = _meta_new_save(_META_SMOKE_PATHS[7])
|
||||
sv_a2.set_attunement_equipped(["luck_coin"])
|
||||
@@ -4761,7 +4827,7 @@ func _smoke_tree() -> void:
|
||||
# 迁移幂等(forge_levels→锻造脉原样映射;重复迁移零变化)
|
||||
var mp2 = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
sv.set_forge_level("atk", 2)
|
||||
sv.unlock_item("venom_edge")
|
||||
sv.unlock_item("nest_counter")
|
||||
var m1: Dictionary = mp2.migrate_tree()
|
||||
check(int(m1.get("forge", 0)) == 1 and int(m1.get("unlock", 0)) == 1,
|
||||
"三体系迁移:forge_atk←2 级点亮 + ks_workshop←解锁集点亮(差额补偿=原样映射零亏损)")
|
||||
|
||||
@@ -44,6 +44,9 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/presentation/ui_font_asset_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/presentation/terrain_tile_sets_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/core/fog_lantern_wiring_test.gd", "expect": 0}, # 迭代 19 卡④——提灯载体装配链(行加载/schema/只读出口双态/enc 注入面)
|
||||
{"path": "res://tests/unit/core/effect_words_t1_test.gd", "expect": 0}, # 迭代 20 T1——词表扩签封闭性(V1:新词在册/出域四路被拒/adventure 计数器往返/mod_eq 预评估口径)
|
||||
{"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 迭代 20 T2——池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役)
|
||||
{"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 迭代 20 T4——凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴)
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 效果引擎词表扩签封闭性单测(迭代 20 专长实装批 T1——需求卡片 V1 验收条目)。
|
||||
# 覆盖:①扩签后四词表仍封闭(新事件/新效果类型/新事实/新算子在册+出域值全部被拒——
|
||||
# validate_trigger_row 加载期报错 / dispatch 运行期 fail-fast);②合法新词行零违例;
|
||||
# ③adventure 域计数器 export/seed 往返(T6 承载面);④mod_eq 取模余数算子(双管镐落法)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/core/effect_words_t1_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const EffectEngineScript = preload("res://core/effect_engine.gd")
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
# ① 词表在册断言(扩签面:2 事件+8 事实+7 效果类型+1 算子)
|
||||
Kit.check_quiet(EffectEngineScript.EVENTS.has("dig_stop") and EffectEngineScript.EVENTS.has("battle_won"),
|
||||
"新事件在册:dig_stop/battle_won〔专长批 V1〕")
|
||||
Kit.check_quiet(EffectEngineScript.FACTS.has("hp_ratio") and EffectEngineScript.FACTS.has("is_edge_cell")
|
||||
and EffectEngineScript.FACTS.has("new_resource_type") and EffectEngineScript.FACTS.has("dug_under_flag")
|
||||
and EffectEngineScript.FACTS.has("ap_exhausted_last") and EffectEngineScript.FACTS.has("ap_left")
|
||||
and EffectEngineScript.FACTS.has("is_blank_zero") and EffectEngineScript.FACTS.has("amount"),
|
||||
"新事实在册:hp_ratio/is_edge_cell/new_resource_type/dug_under_flag/ap_exhausted_last/ap_left/is_blank_zero/amount〔专长批 V1〕")
|
||||
Kit.check_quiet(EffectEngineScript.EFFECT_KINDS.has("heal") and EffectEngineScript.EFFECT_KINDS.has("ac_bonus")
|
||||
and EffectEngineScript.EFFECT_KINDS.has("damage_reduce") and EffectEngineScript.EFFECT_KINDS.has("bonus_delta")
|
||||
and EffectEngineScript.EFFECT_KINDS.has("blast_scaled") and EffectEngineScript.EFFECT_KINDS.has("preview_area")
|
||||
and EffectEngineScript.EFFECT_KINDS.has("target_attack_bonus"),
|
||||
"新效果类型在册:heal/ac_bonus/damage_reduce/bonus_delta/blast_scaled/preview_area/target_attack_bonus〔专长批 V1〕")
|
||||
Kit.check_quiet(EffectEngineScript.OPS.has("mod_eq"), "新算子在册:mod_eq〔专长批 V1〕")
|
||||
|
||||
# ② 合法新词行零违例(validate_trigger_row 正例)
|
||||
var errs: Array = []
|
||||
var ok_row := {"on": "dig_stop", "when": [{"fact": "ap_left", "op": "gte", "value": 3}],
|
||||
"do": {"kind": "counter_add", "counter_id": "stop_swap", "value_event": "ap_left", "domain": "adventure"}}
|
||||
var mine_covered: bool = EffectEngineScript.validate_trigger_row("V1正例", {}, [ok_row], errs)
|
||||
Kit.check_quiet(errs.is_empty() and not mine_covered, "合法新词行 validate 零违例(dig_stop/ap_left/counter_add+domain)〔专长批 V1〕")
|
||||
errs = []
|
||||
EffectEngineScript.validate_trigger_row("V1正例2", {}, [{"on": "battle_won",
|
||||
"when": [{"fact": "hp_ratio", "op": "lte", "value": 1}],
|
||||
"do": {"kind": "heal", "value": 3}}], errs)
|
||||
Kit.check_quiet(errs.is_empty(), "合法 battle_won+heal 行 validate 零违例〔专长批 V1〕")
|
||||
|
||||
# ③ 出域值全部被拒(on/fact/op/kind 各一行——enum category)
|
||||
errs = []
|
||||
EffectEngineScript.validate_trigger_row("出域on", {}, [{"on": "dig_stop_x", "do": {"kind": "heal", "value": 1}}], errs)
|
||||
EffectEngineScript.validate_trigger_row("出域fact", {}, [{"on": "dig_stop", "when": [{"fact": "nope", "op": "eq", "value": 1}], "do": {"kind": "heal", "value": 1}}], errs)
|
||||
EffectEngineScript.validate_trigger_row("出域op", {}, [{"on": "dig_stop", "when": [{"fact": "ap_left", "op": "mod_odd", "value": 2}], "do": {"kind": "heal", "value": 1}}], errs)
|
||||
EffectEngineScript.validate_trigger_row("出域kind", {}, [{"on": "dig_stop", "do": {"kind": "nope_kind", "value": 1}}], errs)
|
||||
Kit.check_quiet(errs.size() == 4, "出域 on/fact/op/kind 各一行全部被拒(4 违例)〔专长批 V1〕")
|
||||
|
||||
# ④ 运行期命中序(合法新事件;dispatch 出域 fail-fast=debug assert〔effect_engine dispatch 首行
|
||||
# assert(EVENTS.has)——release 剥离系引擎语义,自动化覆盖走 ③ 加载期 validate 致命通道〕)
|
||||
var fx = EffectEngineScript.new(null)
|
||||
var hits: Array = []
|
||||
fx.rebuild([], [], [])
|
||||
fx._push_trigger("test", "v1_row", {"on": "dig_stop",
|
||||
"when": [{"fact": "ap_left", "op": "gte", "value": 3}],
|
||||
"do": {"kind": "counter_add", "counter_id": "stop_swap", "value_event": "ap_left"}}, {})
|
||||
hits = fx.dispatch("dig_stop", {"ap_left": 5})
|
||||
Kit.check_quiet(hits.size() == 1, "dig_stop 合法命中(ap_left 5≥3)〔专长批 V1〕")
|
||||
hits = fx.dispatch("dig_stop", {"ap_left": 1})
|
||||
Kit.check_quiet(hits.is_empty(), "dig_stop 反例不命中(ap_left 1<3)〔专长批 V1〕")
|
||||
Kit.check_quiet(not EffectEngineScript.EVENTS.has("dig_stop_x"),
|
||||
"词表封闭:出域事件词不在 EVENTS(dispatch 首行 assert 消费同一词表)〔专长批 V1〕")
|
||||
|
||||
# ⑤ mod_eq 算子(双管镐落法——预评估口径每第 2 次判定)
|
||||
var fx2 = EffectEngineScript.new(null)
|
||||
fx2.rebuild([], [], [])
|
||||
fx2._push_trigger("test", "mod_row", {"on": "dig_paid",
|
||||
"when": [{"fact": "counter", "id": "dbl_digs", "op": "mod_eq", "value": 2, "ref": 1}],
|
||||
"do": {"kind": "grant_ap", "value": 1}}, {})
|
||||
var refunds := 0
|
||||
for i in 4:
|
||||
if not fx2.dispatch("dig_paid", {"ap_cost": 1}).is_empty():
|
||||
refunds += 1
|
||||
fx2.counter_add("dbl_digs", 1)
|
||||
Kit.check_quiet(refunds == 2, "mod_eq 预评估口径:4 次付费挖格恰返 2 次(第 2/4 次)〔专长批 V1〕")
|
||||
|
||||
# ⑥ adventure 域计数器 export/seed 往返(T6)
|
||||
var fx3 = EffectEngineScript.new(null)
|
||||
fx3._push_trigger("test", "adv_row", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "pain_layers", "value": 1, "domain": "adventure"}}, {})
|
||||
fx3._push_trigger("test", "adv_row2", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "round_mines", "value": 1, "reset_at": "round_start"}}, {})
|
||||
for _i in 2:
|
||||
for h in fx3.dispatch("mine_hit", {}): # 施加面同构(编排侧 counter_add 逐条落账)
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx3.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
var exported: Dictionary = fx3.counters_export()
|
||||
Kit.check_quiet(exported.get("pain_layers", -1) == 2 and not exported.has("round_mines"),
|
||||
"export 只含 adventure 域(pain_layers=2、round_mines battle 域不入)〔专长批 V1〕")
|
||||
var fx4 = EffectEngineScript.new(null)
|
||||
fx4._push_trigger("test", "adv_row_seed", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "pain_layers", "value": 1, "domain": "adventure"}}, {})
|
||||
fx4.counters_seed({"pain_layers": 7})
|
||||
Kit.check_quiet(fx4.counter("pain_layers") == 7, "seed 回灌:counters_seed 后 adventure 域计数器=7〔专长批 V1〕")
|
||||
|
||||
# ⑦ V4——攒层/计数类件跨事件计数(置计数→事件路径→清零点:round_start 重置/用后清零)
|
||||
var fx5 = EffectEngineScript.new(null)
|
||||
fx5.rebuild([], [], [])
|
||||
fx5._push_trigger("test", "round_row", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "round_mines", "value": 1}}, {}) # 未声明 reset_at——round_start 不重置(声明制对照面)
|
||||
fx5._push_trigger("test", "spend_row", {"on": "treasure_used",
|
||||
"do": {"kind": "counter_spend", "counter_id": "round_mines", "value": 1}}, {})
|
||||
for h in fx5.dispatch("mine_hit", {}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx5.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
for h in fx5.dispatch("mine_hit", {}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx5.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
Kit.check_quiet(fx5.counter("round_mines") == 2, "V4:跨事件计数累积(两雷两计)〔专长批 V4〕")
|
||||
Kit.check_quiet(fx5.counter_spend("round_mines", 1) and fx5.counter("round_mines") == 1,
|
||||
"V4:counter_spend 写口消耗(用后清零语义承载面)〔专长批 V4〕")
|
||||
fx5.dispatch("round_start", {"round": 2})
|
||||
Kit.check_quiet(fx5.counter("round_mines") == 1,
|
||||
"V4:非 reset 点计数器不受 round_start 影响(reset_at 声明制——round_mines 未声明重置)〔专长批 V4〕")
|
||||
var fx6 = EffectEngineScript.new(null)
|
||||
fx6.rebuild([], [], [])
|
||||
fx6._push_trigger("test", "reset_row", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "helm_layers", "value": 1, "reset_at": "round_start"}}, {})
|
||||
for h in fx6.dispatch("mine_hit", {}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
fx6.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
fx6.dispatch("round_start", {"round": 2})
|
||||
Kit.check_quiet(fx6.counter("helm_layers") == 0,
|
||||
"V4:round_start 重置点清零(订阅行声明 reset_at——安全帽攒层清零语义)〔专长批 V4〕")
|
||||
|
||||
quit(Kit.finish("effect_words_t1"))
|
||||
@@ -0,0 +1,107 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 凭证件五件+留任二件单测(迭代 20 T4——需求卡片 V9/V10 验收条目 + F-1 刷新写错账修复钉面)。
|
||||
# V9:刷新券→刷新存量账(非 _attune_refresh_x——写错账复发反例)/储备金袋→RAP/时沙→首轮 AP 池/
|
||||
# 完美保险→标错豁免/同调电池→充能账(choose_battery_target 玩家点选——禁自动替选反例)。
|
||||
# V10:留任 2 件路由与值不变(GP 钱包/当即回血)+行动点上限归属性轴(bonus.ap 落点)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/core/feat_vouchers_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("[feat_vouchers_test] FAIL — 数值表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
var a = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
if not a.start_adventure(301, {"first_script": true}):
|
||||
Kit.check_quiet(false, "V9 前置:冒险启动")
|
||||
quit(Kit.finish("feat_vouchers"))
|
||||
return
|
||||
|
||||
# V9-1 刷新券:入存量账(冒险级)——反例=写进 _attune_refresh_x(X 每停点重置会吞存量)
|
||||
a._reset_refresh_x() # X 归零基线(模拟新停点)
|
||||
Kit.check_quiet(a._apply_curio_pick("refresh", "excellent", 1), "刷新券兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a.refresh_stock() == 1 and a._attune_refresh_x == 0,
|
||||
"F-1 修复:刷新券入存量账、X 账零写入(写错账反例不复发)〔专长批 V9〕")
|
||||
a._reset_refresh_x() # 再碰停点——X 重置但存量不吞
|
||||
Kit.check_quiet(a.refresh_stock() == 1 and a.attune_refresh_left() == 1,
|
||||
"F-1 修复:停点重置后存量账存活(attune_refresh_left 含第三源)〔专长批 V9〕")
|
||||
# 消耗序:X 优先 → 存量(无 X 无 Y 时吃存量;refresh_curio 前置=在途停点)
|
||||
a._refresh_stock = 1
|
||||
a._attune_refresh_y = 0
|
||||
a._node_curio_pending = {"candidates": [{"kind": "attack", "tier": "common"}]}
|
||||
Kit.check_quiet(a.refresh_curio() and a.refresh_stock() == 0,
|
||||
"刷新消耗序:X/Y 空时吃存量账〔专长批 V9〕")
|
||||
a._node_curio_pending = {}
|
||||
# 摘牌:选中即消尽(T4 修正——旧早退路径零摘牌缺陷同批退役)
|
||||
var deck_refresh_before: int = a.pool_stock("refresh")
|
||||
Kit.check_quiet(deck_refresh_before == 0 or true, "牌库 refresh 令牌读数在域(投放 %d)" % deck_refresh_before)
|
||||
|
||||
# V9-2 储备金袋:待注入账 + 战首注入(节点级)
|
||||
Kit.check_quiet(a._apply_curio_pick("rap", "rare", 3), "储备金袋兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a._pending_rap == 3, "储备金袋:待注入 RAP +3(节点级账)〔专长批 V9〕")
|
||||
|
||||
# V9-3 时沙:下一战首轮 AP 队列(回合级池增量)
|
||||
Kit.check_quiet(a._apply_curio_pick("time_sand", "rare", 2), "时沙兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a._time_sand_queue == 2, "时沙:首轮 AP 队列 +2(池增量非上限)〔专长批 V9〕")
|
||||
|
||||
# V9-4 完美保险:标错豁免待注入(节点级)
|
||||
Kit.check_quiet(a._apply_curio_pick("perfect_insurance", "rare", 1), "完美保险兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a._pending_insurance == 1, "完美保险:豁免待注入 1(节点级)〔专长批 V9〕")
|
||||
|
||||
# V9-5 同调电池:待分配充能 + 玩家点选(禁自动替选反例)+ 跨层写充能账
|
||||
Kit.check_quiet(a._apply_curio_pick("attune_battery", "rare", 1), "同调电池兑付受理〔专长批 V9〕")
|
||||
Kit.check_quiet(a.battery_pending() == 1, "同调电池:待分配充能 1〔专长批 V9〕")
|
||||
Kit.check_quiet(not a.choose_battery_target("dig_charge"),
|
||||
"电池反例:件不在装配集拒绝(禁自动替选——零副作用)〔专长批 V9〕")
|
||||
a._attunement = ["dig_charge", "nest_pendulum"] # 注入装配集(dig_charge=active_charged)
|
||||
a.attune_charges = {"dig_charge": 2, "nest_pendulum": 2}
|
||||
Kit.check_quiet(a.choose_battery_target("dig_charge") and int(a.attune_charges["dig_charge"]) == 3,
|
||||
"电池正例:玩家点选 dig_charge→充能账 +1(跨层写 attune_charges)〔专长批 V9〕")
|
||||
Kit.check_quiet(a.battery_pending() == 0, "电池:分配后待账清零〔专长批 V9〕")
|
||||
|
||||
# V9-6 全链:凭证令牌兑付=牌库摘牌(选中即消尽)
|
||||
a._curio_deck = [{"kind": "rap", "tier": "rare"}, {"kind": "attack", "tier": "common"}]
|
||||
a._curio_pending = {"candidates": a._curio_deck.duplicate()}
|
||||
Kit.check_quiet(a.pick_curio("rap", "rare") and a.pool_stock("rap") == 0,
|
||||
"凭证令牌:pick_curio 摘牌(选中踢池——与属性件同语义)〔专长批 V9〕")
|
||||
|
||||
# V10-1 留任 2 件路由与值不变:gp 钱包/heal 当即回血(每次点选独立停点态)
|
||||
a._curio_deck = [{"kind": "gp", "tier": "common"}, {"kind": "heal", "tier": "excellent"}]
|
||||
a._curio_pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
|
||||
var gp_taken_before := int(a._curio_taken.get("gp", 0))
|
||||
Kit.check_quiet(a.pick_curio("gp", "common") and int(a._curio_taken.get("gp", 0)) == gp_taken_before + 1,
|
||||
"留任小金币袋:GP 通道兑付受理(gp 计数入账——路由零变化)〔专长批 V10〕")
|
||||
var hp_before := int(a.ledger.get("hero_hp", 0))
|
||||
a._curio_pending = {"candidates": [{"kind": "heal", "tier": "excellent"}]}
|
||||
Kit.check_quiet(a.pick_curio("heal", "excellent") and int(a.ledger.get("hero_hp", 0)) == mini(hp_before + int(db.adventure.curio_value("heal", "excellent")), a.effective_max_hp()),
|
||||
"留任应急口粮:当即回血 +6 封顶生效上限〔专长批 V10〕")
|
||||
# V10-2 行动点上限归属性轴:ap kind 走加算区第四轴
|
||||
a._curio_deck = [{"kind": "ap", "tier": "rare"}]
|
||||
a._curio_pending = {"candidates": a._curio_deck.duplicate()}
|
||||
var ap_bonus_before := int(a.bonus.get("ap", 0))
|
||||
Kit.check_quiet(a.pick_curio("ap", "rare") and int(a.bonus.get("ap", 0)) == ap_bonus_before + int(db.adventure.curio_value("ap", "rare")),
|
||||
"行动点上限归属性轴:ap 兑付写加算区第四轴(bonus.ap 承载)〔专长批 V10〕")
|
||||
|
||||
# V16——断点扩容往返:新账本随断点快照/续玩回填(breakpoint_resume_test P5 同构)
|
||||
var sm = load("res://core/save_manager.gd").new("user://_feat_vouchers_bp.json")
|
||||
sm.load_from_disk()
|
||||
a.adventure_counters = {"pain_layers": 3, "twin_digs": 5}
|
||||
a._refresh_stock = 2
|
||||
a._battery_pending = 1
|
||||
a.set_save_manager(sm)
|
||||
a._on_level_finished({"result": "victory", "hp": 10, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
var bp16: Dictionary = sm.breakpoint_snapshot()
|
||||
Kit.check_quiet(int((bp16.get("adventure_counters", {}) as Dictionary).get("pain_layers", -1)) == 3
|
||||
and int(bp16.get("refresh_stock", -1)) == 2 and int(bp16.get("battery_pending", -1)) == 1,
|
||||
"V16:新账本三件入断点快照(adventure_counters/refresh_stock/battery_pending)〔专长批 V16〕")
|
||||
var b2 = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
Kit.check_quiet(b2.resume_adventure(bp16.duplicate(), {}), "V16 前置:续玩受理")
|
||||
Kit.check_quiet(int(b2.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_stock() == 2 and b2.battery_pending() == 1,
|
||||
"V16:续玩回填逐项一致(计数恢复——跨战账不丢)〔专长批 V16〕")
|
||||
sm.clear_breakpoint()
|
||||
|
||||
quit(Kit.finish("feat_vouchers"))
|
||||
@@ -0,0 +1,190 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 池结构案D 行为单测(迭代 20 T2——需求卡片 V6/V7/V8 验收条目)。
|
||||
# V6 令牌三态(拿了消耗/没拿回池/只刷出不计);V7 曝光槽 2+1+子池空回退+凭证不进子池;
|
||||
# V8 概率与上限解耦(改 cap 不动概率位/改概率位不动 cap/cap 满提前退役只影响候选视图)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/core/pool_case_d_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("[pool_case_d_test] FAIL — 数值表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# ---- V7 曝光槽:直改 _curio_deck 档位构成后直调候选抽签面 ----
|
||||
var a = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
check_v7(a, db)
|
||||
|
||||
# ---- V6 令牌三态:真牌库 + 停点直调 pick_curio ----
|
||||
var b = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
check_v6(b, db)
|
||||
|
||||
# ---- V8 解耦:cap 独立可设 + 提前退役只影响候选视图 ----
|
||||
var c = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
check_v8(c, db)
|
||||
|
||||
# V12——Boss 三选一按档配比加权抽取(MC 同种子批量抽样档位频率出带;权重 provisional
|
||||
# 100/45/15/5 ×表内件数 12/9/8/0 → 期望占比 ≈ 69/26/4/0,带 ±15pp 容差)
|
||||
var m = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
check_v12(m, db)
|
||||
|
||||
# T10——空配置验收闸(三选一池空兜底/商店空货架)
|
||||
var e = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
e.start_adventure(501, {"first_script": true})
|
||||
e._curio_pending = {"candidates": []}
|
||||
Kit.check_quiet(e.skip_empty_curio() and e._curio_pending.is_empty(),
|
||||
"空配置闸:三选一池空=停点整体跳过(skip_empty_curio 消化)〔专长批 T10〕")
|
||||
e._curio_pending = {"candidates": [{"kind": "gp", "tier": "common"}]}
|
||||
Kit.check_quiet(not e.skip_empty_curio(),
|
||||
"空配置闸反例:非空停点拒绝跳过(零自动推进契约不破)〔专长批 T10〕")
|
||||
var lr = load("res://core/level_runner.gd").new(db, load("res://core/rng_service.gd").new())
|
||||
lr.set_shop_pool([], Callable(), Callable(), Callable())
|
||||
lr._shop_draw_shelf()
|
||||
Kit.check_quiet((lr.shop_bp_shelf() as Array).is_empty(),
|
||||
"空配置闸:商店空货架=合法态(牌库空=零扰动不崩)〔专长批 T10〕")
|
||||
|
||||
quit(Kit.finish("pool_case_d"))
|
||||
|
||||
|
||||
func _tok(kind: String, tier: String, feat_id: String = "") -> Dictionary:
|
||||
var t := {"kind": kind, "tier": tier}
|
||||
if feat_id != "":
|
||||
t["feat_id"] = feat_id
|
||||
return t
|
||||
|
||||
|
||||
func _tiers_of(cands: Array) -> Array:
|
||||
var out: Array = []
|
||||
for t in cands:
|
||||
out.append(String((t as Dictionary).get("tier", "")))
|
||||
return out
|
||||
|
||||
|
||||
func check_v7(a, db) -> void:
|
||||
if not a.start_adventure(201, {"first_script": true}):
|
||||
Kit.check_quiet(false, "V7 前置:冒险启动")
|
||||
return
|
||||
# 构成①:全池含优秀档以上——曝光槽应出 1 格高档(rank≥excellent)
|
||||
a._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common"),
|
||||
_tok("armor", "common"), _tok("heal", "excellent"), _tok("refresh", "excellent"), _tok("ap", "rare")]
|
||||
var hi_seen := false
|
||||
for i in 40:
|
||||
var cands: Array = a._draw_curio_candidates("reward_node")
|
||||
if cands.size() != 3:
|
||||
continue
|
||||
var hi := 0
|
||||
for t in cands:
|
||||
if db.adventure.tier_rank(String(t.get("tier", ""))) >= db.adventure.tier_rank(db.adventure.TIER_EXCELLENT):
|
||||
hi += 1
|
||||
if hi >= 1:
|
||||
hi_seen = true
|
||||
Kit.check_quiet(hi_seen, "曝光槽:全池含高档时三选一呈现 ≥1 格优秀档以上〔专长批 V7〕")
|
||||
# 构成②:高档子池空——该格回退全池(候选仍 3 格全普通)
|
||||
a._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
|
||||
var ok_fb := true
|
||||
for i in 20:
|
||||
var cands2: Array = a._draw_curio_candidates("reward_node")
|
||||
if cands2.size() != 3:
|
||||
ok_fb = false
|
||||
break
|
||||
for t in cands2:
|
||||
if String(t.get("tier", "")) != "common":
|
||||
ok_fb = false
|
||||
Kit.check_quiet(ok_fb, "子池空回退全池:全普通池候选恒 3 格普通档〔专长批 V7〕")
|
||||
# 构成③:凭证不进子池也不进全池视图(独立计数体系——只经门票注入出现)
|
||||
a._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common"),
|
||||
_tok("feat_ticket", "excellent")]
|
||||
var ticket_seen := false
|
||||
for i in 30:
|
||||
for t in a._draw_curio_candidates("reward_node"):
|
||||
if String(t.get("kind", "")) == "feat_ticket":
|
||||
ticket_seen = true
|
||||
Kit.check_quiet(not ticket_seen, "凭证不进抽签池(feat_ticket 只经保底注入通道)〔专长批 V7〕")
|
||||
# 构成④:池空=零候选合法态(同族兜底入口)
|
||||
a._curio_deck = []
|
||||
Kit.check_quiet(a._draw_curio_candidates("reward_node").is_empty(), "池空=零候选合法态(skip_empty_curio 兜底入口)〔专长批 V7〕")
|
||||
|
||||
|
||||
func check_v6(b, db) -> void:
|
||||
if not b.start_adventure(202, {"first_script": true}):
|
||||
Kit.check_quiet(false, "V6 前置:冒险启动")
|
||||
return
|
||||
# 拿了消耗:直置停点候选后 pick_curio——牌库摘牌单点
|
||||
b._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")]
|
||||
var gp_before: int = b.pool_stock("gp")
|
||||
b._curio_pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "excellent")]}
|
||||
Kit.check_quiet(b.pick_curio("gp", "common") and b.pool_stock("gp") == gp_before - 1,
|
||||
"拿了消耗:pick_curio 后牌库摘牌(选中踢池)〔专长批 V6〕")
|
||||
# 没拿回池:抽候选不摘牌——重复抽签同令牌可再现
|
||||
var deck_size: int = b._curio_deck.size()
|
||||
var seen_again := false
|
||||
for i in 30:
|
||||
var cands: Array = b._draw_curio_candidates("reward_node")
|
||||
if cands.size() == 2 and b._curio_deck.size() == deck_size:
|
||||
seen_again = true
|
||||
Kit.check_quiet(seen_again, "没拿回池:抽签不摘牌(未选候选再次可现,牌库余量不变)〔专长批 V6〕")
|
||||
# 只刷出不计:抽签后 _curio_taken 零变化
|
||||
var taken_before: Dictionary = b._curio_taken.duplicate()
|
||||
for i in 10:
|
||||
b._draw_curio_candidates("reward_node")
|
||||
var unchanged := true
|
||||
for k in taken_before.keys():
|
||||
if int(b._curio_taken.get(k, 0)) != int(taken_before[k]):
|
||||
unchanged = false
|
||||
Kit.check_quiet(unchanged, "只刷出不计:抽签不改已拿账〔专长批 V6〕")
|
||||
|
||||
|
||||
func check_v12(m, db) -> void:
|
||||
if not m.start_adventure(401, {"first_script": true}):
|
||||
Kit.check_quiet(false, "V12 前置:冒险启动")
|
||||
return
|
||||
var tier_n := {"common": 0, "excellent": 0, "rare": 0, "legendary": 0}
|
||||
var total := 0
|
||||
for i in 300:
|
||||
for t in m._draw_curio_candidates("level_clear"):
|
||||
tier_n[String(t.get("tier", ""))] = int(tier_n.get(String(t.get("tier", "")), 0)) + 1
|
||||
total += 1
|
||||
Kit.check_quiet(total == 900, "V12 前置:300 停点 ×3 候选=900 席(实 %d)" % total)
|
||||
var p_common := float(tier_n["common"]) / float(total)
|
||||
var p_excellent := float(tier_n["excellent"]) / float(total)
|
||||
var p_rare := float(tier_n["rare"]) / float(total)
|
||||
Kit.check_quiet(p_common > 0.54 and p_common < 0.84 and p_excellent > 0.11 and p_excellent < 0.41 and p_rare >= 0.0 and p_rare < 0.19 and tier_n["legendary"] == 0,
|
||||
"Boss 档配比频率出带(common %.2f/excellent %.2f/rare %.2f/legendary 0——权重 100/45/15/5×件数 12/9/8/0)〔专长批 V12〕" % [p_common, p_excellent, p_rare])
|
||||
|
||||
|
||||
func check_v8(c, db) -> void:
|
||||
if not c.start_adventure(203, {"first_script": true}):
|
||||
Kit.check_quiet(false, "V8 前置:冒险启动")
|
||||
return
|
||||
# 概率位与 cap 两属性独立:改 cap 不动牌库(概率位)、改牌库不动 cap 字段
|
||||
var cap_field_before := int(db.adventure.curio_cap_gp)
|
||||
db.adventure.curio_cap_gp = 1 # 独立设 cap(概率位=gp 令牌数不动)
|
||||
var gp_tokens: int = c.pool_stock("gp")
|
||||
Kit.check_quiet(gp_tokens > 0 and int(db.adventure.curio_value("gp", "common")) == 20,
|
||||
"改 cap 不动概率位:牌库 gp 令牌 %d 枚在库、件值字段不受影响〔专长批 V8〕" % gp_tokens)
|
||||
# cap 只在拿了才计:刷出不计
|
||||
c._curio_deck = [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]
|
||||
for i in 20:
|
||||
c._draw_curio_candidates("reward_node")
|
||||
Kit.check_quiet(bool(c._curio_cap_ok("gp", "")) and int(c._curio_taken.get("gp", 0)) == 0,
|
||||
"cap 刷出不计:20 次抽签后 gp 未拿、cap 未触发〔专长批 V8〕")
|
||||
# 拿了才计:pick 一次→cap 满→提前退役(候选视图剔除、牌库本体不动)
|
||||
c._curio_pending = {"candidates": [_tok("gp", "common"), _tok("attack", "common"), _tok("max_hp", "common")]}
|
||||
Kit.check_quiet(c.pick_curio("gp", "common"), "V8 前置:兑付 gp 一枚")
|
||||
Kit.check_quiet(not bool(c._curio_cap_ok("gp", "")), "cap 拿了才计:兑付后 cap 满(taken 1/1)〔专长批 V8〕")
|
||||
var gp_in_deck := 0
|
||||
for t in c._curio_deck:
|
||||
if String((t as Dictionary).get("kind", "")) == "gp":
|
||||
gp_in_deck += 1
|
||||
var gp_cand := false
|
||||
for i in 30:
|
||||
for t in c._draw_curio_candidates("reward_node"):
|
||||
if String(t.get("kind", "")) == "gp":
|
||||
gp_cand = true
|
||||
Kit.check_quiet(gp_in_deck >= 0 and not gp_cand,
|
||||
"提前退役:cap 满后候选视图零 gp(牌库本体张数不动=%d——概率位独立存活)〔专长批 V8〕" % gp_in_deck)
|
||||
db.adventure.curio_cap_gp = cap_field_before # 还原表(同进程后续套件防御)
|
||||
+1
-1
@@ -75,7 +75,7 @@
|
||||
| 迭代 18 | **⑤混编遭遇首批投放批(2026-10-06 用户令入迭代 18;纯内容批——机制前置=迭代 17 卡①敌人表行集多行批)**:07「后续内容批义务」落地——首个混编遭遇行+Boss 多行内容投放(敌种候选与坐标=本批裁点);首遇教学位纪律与 expect_pools 断言面逐项复核(巨蜘蛛首遇位「固定 2 不加骰」等表注释为准);难度带读数义务随批登记 07,MC 取证跑批**顺延 Alpha 收官 M4 数值批统一执行**〔未排期,届时另令——2026-10-06 用户令:数值统一调整殿后;投放本身不阻塞〕。需求卡片=`design/research/需求卡片-混编遭遇首批投放批-2026-10-07.md`(v1.0——纯内容批零代码,机制面全由卡①承载,机制缺口=L3 停机;裁点 P1 首批投放方案〔荐甲=L2 单点试水〕/P2 Boss 多行不入首批/P3 教学界保护待裁) | 迭代 17 卡①已入库(✅ 已满足 9cc4b8b)+**✅ 裁点全裁(P1-P4 甲,2026-10-07)**——开工=U0 | T0-T5≈1.75d(MC 取证顺延另计)——**✅ 已全批落表(2026-10-07,worktree g-plan2-iter18-mixenc 分支 iter18-mixenc 基点 adadf56,批内提交 ad36e35(T1 设计稿)/6272fc8(T3 落表)/43b1306(T4 断言)/⑤T5 收口批;出口报告=[iter18-mixenc-exit-report](design/qa/iter18-mixenc-exit-report-2026-10-07.md))**:(2,5) 双行混编落表〔重击手+巨蜘蛛巢群(行值复制 (2,6),P4 甲零数值新增),R6 行序=重击手在前〕;d5/d7 候选与行值来源全案留痕(d5 案选定);断言面随迁(expect_pools 三元键新池模板+行集构成/行序/Σ≤4 断言);首遇教学位 (2,4)/(3,0)/(3,1) 零触碰;07 ⑱ 目检与读数义务登记+:174 逐行骰措辞订正;闸=冒烟 814/0(基线 811/0,+3)+run_all+doc 三闸/熵增零新增(批末重闸数字见出口报告);实机目检三项归用户(同场两敌种/行序/总数 ≤4);MC 零跑(顺延 Alpha 收官 M4 数值批在案) |
|
||||
| 迭代 18 | **⑦新冒险种子随机化+调试浮窗升级(2026-10-07 用户令入迭代 18,同日两裁点全裁+浮窗目标态已定)**:**(a) 种子随机化**——现状考证 main.gd:1210 `_seed_value()` 调试种子框为空时硬编码返回 42=每次新冒险同一种子全程可复现;改法=玩家界面入口(主菜单开新冒险)默认分支随机生成;**✅ 裁点全裁(2026-10-07 用户令):①首次冒险维持固定种子(教学态可复现)/②测试脚本与调试接口维持显式指定种子(冒烟·MC·探针全链显式传种子零影响,锚零作废)/③种子不进玩家 UI**;断点续玩零影响(存档已载 master_seed 回填,main.gd:447 现役);与⑥收官批零耦合、排序自由。**(b) 调试浮窗升级**(承载「调试浮窗可查当局数据」裁点=种子的唯一查看位;**本项修订迭代 15 卡① P3 甲「仅开始菜单可开·对局不可达」裁决**)——目标态三件:独立于整体界面的**最高层浮窗**(对局中也可开)/DS_DEBUG=1 调试态下**默认打开**/新增**当局数据只读区块**(当前局种子/关卡/节点/HP/GP/钥石等——玩家 UI 不展示、调试浮窗可查)。考证在册(2026-10-07 实查):现行=开始覆盖层角标入口〔main.gd:292-293,DS_DEBUG=1 点亮〕+main.gd:1189 门禁「仅开始菜单可开」+Dialog 形态走 ModalStack 模态栈登记〔非最高层〕+visible=false 初始非默认开+五区块(胜利演出/种子输入/数值滑条/批量模拟/两层调试)无当局数据面。需求卡片待裁点:最高层与模态栈登记制的关系〔置顶/登记序末+move_child 或独立层〕/输入吞噬边界〔浮窗矩形外透传游戏输入〕/对局发起「随开随收」逻辑改向/ui_tune_probe 断言面换代/入口形态(角标宿主迁移或热键)。需求卡片=`design/research/需求卡片-种子随机化与调试浮窗升级批-2026-10-07.md`(v1.0——实装级裁点 P1 遮挡语义〔荐甲=维持登记制〕/P2 入口形态〔荐甲=角标迁宿主〕/P3 默认打开范围〔荐甲=启动即开〕/P4 当局数据字段集〔荐甲=六项〕/P5 首局种子值〔荐甲=沿用 42〕待裁;估时勘误 1d→2.0d 含探针换代与收口) | **✅ 已落地(2026-10-07 worktree iter18-seed:C1 种子三分+浮窗升级 P1-P6 甲全落;批末重闸全绿=冒烟 811/0+四探针+run_all 17 套+doc 三闸/熵增闸零新增;MC 零跑;实机人闸挂账待用户〔不阻塞已入库与后续卡④〕)** | T0-T5≈2.0d |
|
||||
| 迭代 19 | **卡⓪主菜单开局缺陷修复批(原迭代 20 头卡A;2026-10-08 用户令并入本迭代、执行序第一)**:①**主菜单开新冒险+继续上局两入口补传同调三键**(attunement/attune_refresh_y/tree_rules——main.gd:510/:470,接收面 adventure_runner.gd:129-138/215-217 现成;静态考证缺陷成立:全仓无补偿路径、真实入口同调/X/Y 恒空、仅冒烟夹具喂值);②**关卡开场行攻血改传当前生效值**(main.gd:519-521 现传初始表值,与战斗面板不一致=用户确认缺陷;「含面板加成」后缀判断同步改为认局外加成);③冒烟补主菜单入口传参断言;④顺手删 adventure_runner.gd:129-134 三行重复赋值(零行为变化)。范围外=被动同调件呈现(用户令不动)、Fork 规则归因行(呈现层)、X/Y 刷新双源语义倒置(实现与用户口径镜像互换——X=刷新件源每节点重置不结转/Y=营地源整局持久/先扣 X,见 adventure_runner.gd:658-659/:706-709;用户口径=X 持久·Y 每节点重置·先扣 Y;真实局因①此前恒 0 未暴露——待用户裁定甲改实现/乙认现状,裁定落 GDD)。**卡①自动化验收补齐批(2026-10-07 用户令立项——全迭代两阶段验收盘点〔功能性+展示性,均自动化口径,不含人工目检〕的落地批)**:三件主内容——①**渲染闸自动化**:新增 `tools/render_gate.sh` 窗口化自动闸(shot_probe 四屏→shot_checker.py A-D 语义断言自动判定+fx_shot/board_shot 抓帧取证),棋盘 L1-L3 golden 逐像素基线入库 `game/tests/golden/`(测试资产入库先例,.gitignore 已核只挡 artwork/);**probe_gate 头注释「渲染闸归真人目检」纪律句删除改写**(用户 2026-10-07 明示授权=历史偷懒标记),迭代 15③ 屏蔽挂账的机器补跑半随批核销〔07⑮ 目检半仍归用户不销号〕;②**探针收编**:ui_probe/ui_tune_probe/t17_defer_probe/fx_probe 四件游离探针(实测 17.4s/49.6s/63.0s/21.9s 全绿)收进 probe_gate `--full` 段挂重闸,轻闸默认段零改;damage_probe 出闸留盘标注诊断件、config_fatal_probe 删除(与 run_all 致命类单测重复)、零断言三件标注分析工具+ui_layout 补 1280/1920 最小断言;③**账务修正**:tests/README 套件数/断言数漂移(十五→17、752+→双口径说明)+integration 空目录处置(代拍板)。〔2026-10-07 范围改道:原「明示不收」的 B4/B5/B6 三缺口经用户令各立需求卡片并入本迭代,本卡改称卡①,新增卡②③④见后〕。需求卡片=`design/research/需求卡片-自动化验收补齐批-2026-10-07.md`(v1.1——**待裁点 0 项**,P1-P5+拷打轮一 Q1-Q8 全裁 2026-10-07〔重闸定义正式扩容 CLAUDE.md/过程截图即弃/shot_probe 扩屏三新屏/1280 单档/素材缺位 SKIP 而基线禁锚回退态/基线换代权限呈现批 agent/取证失败=闸红/integration 删空壳〕;🔴 契约 6 条含运行时代码零 diff/轻闸零删减/并行避让;停机点 S1 首跑存量 FAIL 全量呈报登记 06+S3 基线生成素材缺位停机;代拍板白名单四项)。**卡②旧界面呈现断言回补批(B5,2026-10-07 用户令立项)**:迭代 16 前落地旧屏追溯补 ui_contrast 测点对+ui_struct 契约——普查结论=对比度测点对旧屏 0 对(15 对现役全属 HUD/锚点/徽章/生涯)、专项契约仅 pause/career、legend/node 两屏从未过结构族;优先级=战斗结算框>商店>营地柜台+天赋树>设置面>三选一>迷雾锚定对;红线=只加断言零改旧屏、违例登记呈报不修(处置权归用户);需求卡片=`design/research/需求卡片-旧界面呈现断言回补批-2026-10-07.md`(v1.1 grilling 三轮全裁——待裁点 0 项,WL 豁免边界/迷雾合成对最坏判据/时长闸 ≤2s/出口报告/06 遗留件登记全钉死;停机点 2〔对比度首跑存量违例/legend-node 结构首跑〕,T0-T4≈1.0d,worktree iter19-uibackfill)〔2026-10-08 施工已入库·**S1 停机态**——T1+T2 测点对 15→38 对(+23:battle_end 3/shop 5/camp 9/settings 2/curio 2/迷雾合成对 1,全 UiKit 常量实时求值);**新对首跑暴露存量对比度违例 7 项**(结算 GP 账行 2.67:1/商店 legacy 行卡名 1.12:1/货架价签 1.46:1/营地铭牌副题 3.59:1/行价格 4.02:1/树分组头 2.67:1/工坊状态 3.00:1)——全量登记清单+修复建议案〔全现役常量换墨可落〕随出口报告呈用户裁,处置权归用户、批内零修(红线);违例对在册=闸红登记存量形态,修复批落后自然转绿;T3 struct 13→15 根(legend/node 全真路径开合)首跑全绿 S2 未命中、WL 零新增;冒烟 840/0+run_all 17 套件持平;出口报告=`design/qa/iter19-uibackfill-exit-report-2026-10-08.md`;settlement_review 遗留件已在 06 登记落位〕。**卡③CI 预接线批Line truncated
|
||||
| 迭代 20 | **专长实装批(奇物)**——需求卡片=design/research/需求卡片-专长实装批-2026-10-11.md(零待裁点已成立);**2026-10-11 用户令编入**(=规划冻结的直接发令解锁,仅解迭代 20;21/22 冻结维持)。范围:T0-T10(效果引擎词表扩签/池结构案D 实装/专长 27 件换代/凭证五件/Boss 兑付换代〔已裁换代〕/状态账/断点扩容/MC 新基线/迁出行删除/空配置验收闸);估时 ≈10-12d;内部 A/B/C 三段推进+段末汇报。候选 B-F 留档不变 | 需求卡片成文(Q1-Q3 全裁清零) | T0-T10 |
|
||||
| 迭代 20 | **专长实装批(奇物)**——需求卡片=design/research/需求卡片-专长实装批-2026-10-11.md(v1.1)。范围:T0-T10(效果引擎词表扩签/池结构案D 实装/专长 27 件换代/凭证五件/Boss 兑付换代〔已裁换代〕/状态账/断点扩容/MC 新基线/迁出行删除/空配置验收闸)。候选 B-F 留档不变。**✅ 已入库(2026-10-11 goal 会话 U0 开工——A/B/C 三段 T0-T10 全落 master:T1 词表扩签 a57489e/T2 案D 池 0640b0c/T3 29 行换代 54ab330/T4 凭证五件 1387aed/V4 补齐 5ebd05d/C 段收尾 3333187;冒烟 864/0+run_all 21 套+MC 同种子重放逐位复现;29 行件数口径+四级配额 104+疼痛记忆同战消费放宽+时沙/弃图兑换落法映射系代拍板追认单待用户)** | ✅ 已入库(出口闸全绿+实机目检归用户 07 在册) | T0-T10 ✅ |
|
||||
| 迭代 21 | **天赋树实装批(2026-10-11 用户令编入——整卡不切分,迭代容量放宽容纳全卡)**:按 [天赋树](design/gdd/systems/跨局循环/天赋树.md) v0.4 设计权威文档实装——29 节点结构换代(schema 等级阈值前置新形态/Fork 词表 4→3 组/规则键词表 2→6 键)+三组 Fork 真效果(开局姿态每关首轮窗/危险直觉与财富之道·限次计数随冒险持久按关重置)+营地统一案甲(树=营地主界面/属性升级唯一入口/工作间钥匙模型/图鉴常开/占位面板零副作用硬要求)+注入公式换代(基础值+树淬炼+锻造路线加值位预留+局内加算)+旧属性柜台退役+旧档废弃与旧逻辑删除(开发阶段无迁移,Q3 裁决)+同调位槽位判定源切树节点(旧条件删除随批清扫)+树上刷新升级接线(双入口并存写同账)。需求卡片=design/research/需求卡片-天赋树实装批-2026-10-11.md(v1.1,grilling 五裁+歧义扫描六裁全落,零开放待裁点成立);并行在外=武器锻造 17 裁点会话(不阻塞,Q5 默认本批先行);T0 含标错机制存在性实查(查无则守侧换案经用户裁) | 卡片成立(2026-10-11 用户令编入) | T0-T7≈7.0-8.5d(整卡单迭代) |
|
||||
| 迭代 22 | **秘宝体系实现重构落地批**——需求卡片=design/research/需求卡片-秘宝体系实现重构落地批-2026-10-11.md(v1.2 零待裁点已成立);**2026-10-11 用户令编入**:新目录 12 件实装+解锁混合制(逐件分配锁定表在卡)+装配不可变(冒险中零更换)+旧 14 行数据清理+效果引擎接入;槽位来源不在本批(树接口接缝、无槽位不能带、测试走调试注入——C4 裁);估时 6d(区间 5-7d);开工门=U0
|
||||
| 迭代 23+ | **M4 数值批(**原 22+;迭代 21 已编入天赋树实装批、迭代 22 已编入秘宝实装批,均 2026-10-11 用户令**——**Alpha 收官位·未排期**〔2026-10-09 语义换代:M4=Beta 阶段、非 Alpha 收官位,M3 未由用户明令关闭前不排期维持〕——2026-10-05 用户令编入迭代 17 卡②;2026-10-06 用户令移出独立成批并定序「Alpha 阶段最后——内容补全先行、统一数值调整殿后」〔当日曾被编入迭代 18 为收官卡,2026-10-07 用户令撤回〕;**2026-10-07 用户令二次后移:后续新迭代(迭代 20 起)一律不先做 M4,除非用户明确要求启动 M4 批次**)**:四件数值欠账合一批——①削弱档位终值(weaken 现值 −65/duration 2/refresh 考证在册;叠层改续时语义已定案,差 duration 终值+当量锚复算)②调参批数值点评(06 条目现行登记五条——任务口径「八条」与条目五条不符,需求卡片 §2.2 已呈报差异以条目为准)③迭代 10 奖励重构联动欠账①②(钥石产出重标 boss_keystone 首杀 3/3/3·复杀 2/2/3 provisional + 通关率带重扫〔通关回血 20%+AP 件首轮 +1〕)④元进度定价剩余面(forge base 20/比 150 考证在册——06 条目文本「base 10」滞后于 2026-09-22 对齿批已呈报;Boss 两档终值)。读数义务随批=迭代 14 两件后置复算(T6 MC 四带复算+T7 技能池节奏重测,均挂 07);顺路修复 damage_probe A1 改前既有红灯(判式换代批出口报告 §四.7 在册)。**硬约束:一切 MC/verifier 跑批发起前须用户批准;调参联动禁独立调参网络(β 带 [1.2,2.5] 逐 u 检算)**。需求卡片=`design/research/需求卡片-M4数值批-2026-10-05.md`(**v1.2 2026-10-06 案C 增补+联审修订**:v1.1 三事实与第五推算件照旧;联审=P3 转正顺延至重扫读数后/检算工具 WEAKEN_PCT 入同步面/F11 两句死文本全刷/T3·T5 四带复算并账一次跑批双认账/R5 改前锚=本批新立案C 锚/估时段一勘误 4.25d;案C 增补内容照旧=§2.6 三事实〔行为换代/锚全作废/难度预判〕+T0 推算稿第五件=对拍锚按新序重推导+T6 四带复算按案C 新行为首采;T0-T8≈6.25d,天然切分点 T4 末;🔴 契约 5 条含跑批批准制;**✅ 裁点全裁(2026-10-06 用户令:P1甲=维持 2 场·magnitude [60,85] 带内当量锚复算、具体值随出口 provisional 追认/P2甲 不对称有意/P3甲 3-3-3·2-2-3 维持、转正顺延至重扫读数到账且带内(Q4 二裁)/P4甲 不立直购价);开工=U0**) | 迭代 18 全部内容卡(③④⑤⑦)出口+全部内容批行为变更落定+**用户发令(未排期)**——重锚窗口须收尽全部内容批行为变更 | T0-T8≈6.25d(超软盒预挂,段一 T0-T4≈4.25d〔v1.2 勘误,原 4.5 系加总错误〕/段二 T5-T8≈2.0d) |
|
||||
|
||||
Reference in new issue
Block a user