iter10 追加批: 奖励节点三选一共享池(用户拍板)——AdventureRunner 节点API+AuxItems 句柄面+main 共用面分派+node_overlay 道具面退役+autoplay 节点驱动+reward_overlay_test 随面迁移+a裁备选(L3复杀3)+g裁semver(v2兼容断言+存档序列化v1.3)+f裁调研待办(06新条)+文档五件+任务书(冒烟628/0+run_all 15套+五探针+三闸零新增+熵增PASS)
This commit is contained in:
1 parent
93e4764ca0
commit
4f62a489b6
19 files changed
+377
-154
No files matched your search
@@ -0,0 +1,34 @@
|
||||
# M-3 迭代 10 追加批任务书:奖励节点共享池 + 七项代拍板裁决(2026-10-01)
|
||||
|
||||
> 触发 = 用户 2026-10-01 对迭代 10 代拍板 7 项的逐条裁决 + 新增范围指令(「奖励节点其实也是一个三选一的奖励获取,和通关关卡得到的这个三选一奖励是相同的,他们共享同一个池子;把需要补充的文档,还有迭代的任务书和代码实现都补充上」)。本批在迭代 10 worktree(m3-iter10-20261001)内执行,不合入 master、渲染层验证冻结口径不变。
|
||||
|
||||
## 一、裁决记录(2026-10-01 用户逐条)
|
||||
|
||||
| # | 代拍板 | 裁决 |
|
||||
|---|---|---|
|
||||
| a | Boss 钥石子选项 | **用备选**:复杀按关 2/2/3 递进(首杀 3 三关统一;L3 3/3 系 first≥repeat 合法端点)——原代拍板「复杀统一 2」推翻,meta.tres/registry 随改 |
|
||||
| b | 三选一件值 provisional | 认同(1/5/1/GP20/AP1/道具 1 件,归调参批重投) |
|
||||
| c | AP 件兑现语义 | 认同(每场战斗首轮基线 +1) |
|
||||
| d | 钥石增量行呈现位 | 认同(战斗总结结算框;三选一面零钥石) |
|
||||
| e | 同调位过渡形态 | **待白话说明后追认**(本批先按现行形态保留;追加批已把奖励节点渠道升级为替换子交互两路——原「满位不授予」收窄面随共享池消除) |
|
||||
| f | MC 三选一策略 | 暂定维持 + **登记调研待办**(06 新条目):随机选取奖励是否会对角色能力有较大影响——首件 vs 按权重随机双窗对照 |
|
||||
| g | save 版本兼容 | 对齐 semantic version 向后兼容口径:旧档可读可玩、断点可续、缺新键合法空态;v2→v3 兼容断言随批(存档序列化.md v1.3 成文) |
|
||||
|
||||
## 二、新增范围:奖励节点三选一共享池
|
||||
|
||||
**设计**:奖励节点到达即开与通关三选一相同的选择面——提升池六种同权重、同全冒险次数上限(两渠道合计判上限)、同 reward_pick 封闭流;道具件满位触发替换子交互(与通关面同构);「池空直通」合法态退役(GP/道具件无上限,牌库恒非空)。
|
||||
|
||||
**落地面**:
|
||||
- AdventureRunner:node_bonus_candidates / present_node_bonus / apply_node_bonus / resolve_bonus_replace(分发节点句柄与通关序号两轨)+ _node_bonus_pending(断点不载——续玩重建重抽,计数器保证同候选)
|
||||
- AuxItems:holding_records / discard_item(节点替换的库存句柄面)
|
||||
- main.gd:奖励节点分支改走 BonusPickOverlay 共用面(_bonus_context 分派 continue);node_overlay 道具三选一面退役(保留恢复点用途);_reward_node_choices/_on_reward_item_chosen 退役
|
||||
- autoplay:节点三选一确定性驱动(首件/放弃新件)+ resolved 上下文分派
|
||||
- narrator:node_bonus_header
|
||||
- 冒烟:⑫ v2 兼容三断言 + ⑬ 节点共享池三断言 + ② 节点计数/上限集扩;_adv_drive_to_shop 节点驱动
|
||||
- reward_overlay_test 随面迁移改写(守 BonusPickOverlay:两段式/确认外发/替换两路/结果态)
|
||||
|
||||
**文档**:关卡节点地图.md(节点奖励节改写+行表)、冒险结构.md(提升池冒险级单例句)、node-reward.md v2.0(重写)、魔法道具.md v0.7(渠道改写注记)、存档序列化.md v1.3(semver 口径)、06(调研待办新条)、registry(meta_table a 裁备选值)
|
||||
|
||||
## 三、验证义务
|
||||
|
||||
冒烟全量 + run_all 15 套 + 五探针 + doc 三闸零新增 + 熵增闸 + MC 重锚三跑(追加批改经济面——读数报告更新 v2 段)。
|
||||
@@ -266,6 +266,14 @@
|
||||
|
||||
**状态**:已登记(2026-10-01,迭代 10 批随批)
|
||||
|
||||
#### 奖励随机选取对能力面影响调研(迭代 10 追加批 f 裁决登记,2026-10-01)
|
||||
|
||||
**现状**:迭代 10 追加批用户裁决——MC 自动模拟三选一策略(候选取首件/替换放弃新件)暂定维持;若后续发现随机选取对角色能力有较大影响再调整策略。**调研事项 = 随机选取奖励是否会对角色能力有较大的影响**:autoplay「首件策略」vs「按权重随机策略」的通关率双窗对照(同种子配对读数,方法仿换算率 OFAT 配对先例;随机策略需新增取数签防污染 reward_pick 复放面——设计随调研批)。
|
||||
|
||||
**归属**:曲线调参批(现行窗)+ 分析侧
|
||||
|
||||
**状态**:已登记(2026-10-01,迭代 10 追加批随批)
|
||||
|
||||
#### 关卡内结构随机生成(哈迪斯 / 哥布林弹珠式布局变化)
|
||||
|
||||
**现状**:用户 2026-09-20 方向性提及布局随机变化(哈迪斯 / 哥布林弹珠式)。**2026-09-27 拍板立项**,设计文档 [关卡节点图生成](systems/冒险循环/关卡节点图生成.md) v0.3:图模板池逐局抽签+三级台阶+测量对账;五拍板点全部裁决落文——①首关参与抽签仅首次冒险固定脚本④池每关 5 张⑤逐关抽确认②统一用一个流(rng 架构批先行,ADR-0007)③机制先行骨架随后。实施排队架构批裁决后。
|
||||
|
||||
@@ -98,7 +98,7 @@ Boss 兑付(胜利或英雄死亡)触发关卡收束点,此后不再有选
|
||||
|---|---|
|
||||
| 战斗 / Boss | 组装定位信息(关卡序号 + 节点编号),移交[战斗结算](../战斗循环/战斗结算.md)开战;战斗结果(胜负与战后生命)经战斗结果上报回流冒险结构(契约语义归[冒险结构](冒险结构.md),本文不复述) |
|
||||
| 恢复 | 向冒险结构发治疗封顶回报(载荷 = 该关恢复比例,读本关配置行);按比例回血并封顶的执行归冒险结构 |
|
||||
| 奖励 | 触发节点奖励兑付:获得一件功能道具并入持有域(见下) |
|
||||
| 奖励 | 触发节点奖励兑付:提升三选一(与通关三选一共享同池——六种加权抽 3 点选一件,同次数上限;见下节) |
|
||||
| 商店 | 打开商店覆盖层(商品陈列与定价读商店表);购买即时生效、写统一冒险加算区(与关卡大奖励同一本账——账本语义归[冒险结构](冒险结构.md),本文不复述);离开覆盖层即兑付确认。商店房不携带战斗行:零敌人、零棋盘、零战斗初始化 |
|
||||
|
||||
**遭遇构成的编排面**——本文对"放什么敌人"的全部职责只有三条:
|
||||
@@ -109,7 +109,7 @@ Boss 兑付(胜利或英雄死亡)触发关卡收束点,此后不再有选
|
||||
|
||||
第二关的"新内容 = 既有池新技能"([技能池提案](../../../research/proposal-skill-pool.md)已定)投放时,改的是战斗侧行里引用的技能池内容——节点图结构原样不动,本文无需为投放留任何新结构位。
|
||||
|
||||
**节点奖励 = 魔法道具投放渠道(现行=切片期过渡形态)**:现行兑付(2026-09-29 迭代 7 拍板②丙)= 奖励节点到达即开**三选一构筑点**(呈现 spec:[node-reward.md](../../ux/node-reward.md))——从可用道具池取一件加入关卡域库存,选择前不给出口;池空直通**零兑付是合法空态**,不是缺陷。道具内容、可用池口径与使用窗口语义归[魔法道具](../排雷循环/魔法道具.md)。**形态沿革(2026-09-30 用户裁决甲)**:本渠道系切片期过渡形态,终局=魔法道具局外同调件(底稿 proposal-magic-item-attunement-2026-09-22.md),随同调实施批退役改写;旧「功能道具的正式渠道」拍板(2026-09-17)已被现行三选一口径取代。
|
||||
**节点奖励 = 提升三选一(迭代 10 追加批——用户 2026-10-01 拍板:与通关三选一共享同池)**:奖励节点到达即开三选一(呈现 = 提升三选一覆盖层共用面,spec:[node-reward.md](../../ux/node-reward.md) v2.0)——从提升池六种(攻击/生命上限/护甲/GP 袋/魔法道具/行动点件)按同一加权洗牌抽 3(同权重、同全冒险次数上限、同 reward_pick 封闭流),玩家点选一件兑付;道具件满位触发替换子交互(与通关三选一同构)。件获取次数计数与牌库剔除态为冒险级单例——两渠道合计判上限。原「魔法道具投放渠道三选一」(迭代 7 拍板②丙过渡形态)随批退役;终局=魔法道具局外同调件的改写义务仍归同调实施批(proposal-magic-item-attunement-2026-09-22.md §9)。「池空直通」合法态随共享池退役——池含无上限 GP/道具件,牌库恒非空。
|
||||
|
||||
### 关卡收束点:先清库、后宣布
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗
|
||||
| 魔法道具件(随机一件入库存) | 道具授予既有路由入库存 | 受同调位约束(见下) | 1 | 无次数上限(受同调位) |
|
||||
| 行动点件(AP +1/战) | 加算区行动点轴累加 | 每场战斗开始首轮行动点基线 +1 | 1 | 2 次(全冒险最紧) |
|
||||
|
||||
**抽取机制 = 加权洗牌不放回**(禁等概率真随机):weight-key 洗牌(每件 key = U(0,1)^(1/权重) 取前 3,Efraimidis-Spirakis),单次三选一零重复、逐件按权重出概率;消费 reward_pick 封闭随机流(主种子派生、[数据配置](../表现与支撑/数据配置.md)口径),同种子同操作序列同三选项可复放。达任何次数上限的件**先剔出牌库再洗牌**(AP 用尽 2 次、属性件达 3 次后永不再现)——防「三选项全不可选」死角(GP/道具件无次数上限,牌库恒非空)。
|
||||
**抽取机制 = 加权洗牌不放回**(禁等概率真随机):weight-key 洗牌(每件 key = U(0,1)^(1/权重) 取前 3,Efraimidis-Spirakis),单次三选一零重复、逐件按权重出概率;消费 reward_pick 封闭随机流(主种子派生、[数据配置](../表现与支撑/数据配置.md)口径),同种子同操作序列同三选项可复放。达任何次数上限的件**先剔出牌库再洗牌**(AP 用尽 2 次、属性件达 3 次后永不再现)——防「三选项全不可选」死角(GP/道具件无次数上限,牌库恒非空)。**提升池为冒险级单例**(迭代 10 追加批):关卡内奖励节点的三选一与通关三选一共享同一池、同一权重与同一次数上限(两渠道合计判上限),兑付语义归[关卡节点地图](关卡节点地图.md)。
|
||||
|
||||
**魔法道具件受同调位约束**:道具持有上限 = 同调位数。本批挂过渡读数源 transitional_sync_slots(provisional 初始 1——首次冒险即可持 1 件道具,L1 通关大奖励即可体验道具件);正主 = 同调位里程碑谓词表([proposal-magic-item-attunement-2026-09-22](../../../research/proposal-magic-item-attunement-2026-09-22.md):0 起步、硬上限 3、增长 = 通关次数 + Boss 击杀数走 attunement_growth.tres),同调实施批落地后切换读数源为谓词表实时求值。获取渠道约束形态(迭代 10 过渡承载):三选一道具件超位触发**替换交互**(现有件与新件 N+1 选 N——丢任一原有件腾位收新,或放弃新件不获取;终态恒 ≤ 同调位);奖励节点与战斗内渠道超位**不授予**(候选拒绝 + 掉落沉没,逐行归因留痕)——停点渠道全覆盖的替换交互随同调实施批统一收口(06 在册)。
|
||||
|
||||
|
||||
@@ -201,3 +201,4 @@ playtest 预警(预注册,归难度曲线与Boss):
|
||||
| v0.4 | 2026-09-17 | 数值纪律收口批:正文与调参表 knob 现行值改指路存根(事实源 data/tables/*.tres 与 registry),1 处失同步修正(AP 8→6 漏改:爆破工兵包定位读数段的「每轮行动点默认 8」改指路 action_economy.tres)——道具加量、供给读数、调参表六行 w/t/mag、道具依赖预警阈值 50% 均改指路 registry `aux_items`;行名与表结构列保留 |
|
||||
| v0.5 | 2026-09-29 | **内容批落地修订(迭代 7——用户八拍板)**:①道具定义表六行入表(AuxItemTable——三有效实装:工兵令 ap_grant/探巢摆钟真视/爆破工兵包组合;**探宝罗盘退役留痕**(拍板①乙——未开格类型微光常显覆盖预读价值,retired 标记词表兼容);显影尘/地穴全图卷 deferred「能力封存」(拍板④甲,Alpha 重定);②F2 维度覆盖改**投放维度覆盖**口径+豁免面 {plan, info, counter} 显式登记(罗盘退役+显影件悬置所致,豁免随 Alpha 重定/天赋批消减);③解锁语义定稿=工坊解锁→进掉落投放池(拍板③甲,未解锁权重 0);④奖励节点=三选一构筑点(拍板②丙,node-reward.md v1.0);⑤掉落投放三件总权重 10/池 88(拍板⑥甲,provisional) |
|
||||
| v0.6 | 2026-09-30 | **拍板点 1 收口批(2026-09-30 用户裁决甲:同调=终局方向)**:①新增「定位与形态沿革」节——现行正文定性=切片期过渡形态、终局=局外同调件(设计权威底稿 proposal-magic-item-attunement-2026-09-22.md 八拍板),两态关系=形态替换、衔接纪律与事故留痕入节;②正文口径回写四处:五维度表信息/规划/反制三行改现行态(罗盘退役/显影件封存/豁免面入正文)、覆盖检查改投放维度覆盖口径、掉落池获取节补解锁门控(2026-09-29 拍板③甲)、板级叠加层预读集改「机制在案无投放来源」;③验收两条(护栏/覆盖检查)与调参表投放态注记、地形变异依赖行同步;④实施批登记见 06「魔法道具同调实施批」条。审计冲突 C19/C29 随批收口(全录见 design/research/gdd-concept-coverage-audit-2026-09-30.md) |
|
||||
| v0.7 | 2026-10-01 | **奖励节点渠道改写注记(迭代 10 追加批——用户拍板「奖励节点与通关三选一共享同池」)**:①「奖励节点三选一构筑点」形态(v0.5 ④)退役——奖励节点改提升三选一共享池(魔法道具件为池中 1/6 权重选项,同调位满位替换交互随 BonusPickOverlay 共用面生效,spec:node-reward.md v2.0);②道具件经节点渠道入本关库存(来源归因 reward_node,与掉落/三选一渠道同受 transitional_sync_slots 过渡上限约束);③战斗掉落渠道零改动;④本批过渡约束形态与切换义务注记见 registry aux_items 条同日批注;同调实施批的全渠道统一收口义务不变 |
|
||||
@@ -169,3 +169,4 @@ MVP 不要求本系统的闭环([不做清单](../../05-不做清单.md)把单
|
||||
| v1.0 | 2026-09-21 | **存档序列化批(SaveManager 落地)**:格式裁决 JSON 单档 `user://save.json`;数据模型逐域成文(元进度四件 + 最近冒险摘要 cap 10);读写时机(启动读盘/冒险收束写盘/营地即时写/测试入口不写)+ tmp+rename 原子写;版本策略(高版本拒读禁写/低版本链式迁移/缺域补齐);损坏兜底(.corrupt.bak 备份 + 默认新档);待设计问题清单六条全部裁决成文(1/2/3/5/6 入核心规则,4 = 单档 MVP 多档留 P2)。自范围草案升定稿 |
|
||||
| v1.1 | 2026-09-23 | 单关测试直入口(L2/L3)退役(用户拍板——唯一试玩入口 = 完整冒险):不写档条款改口径为「调试两层与 headless 单关测试路径(LevelRunner.start_level,仅 QA 工具)」;写盘面零改动(正式冒险收束唯一写档点不变) |
|
||||
| v1.2 | 2026-09-29 | **断点域出生(迭代 8 卡①,schema v1→v2)**:`breakpoint` 域登记(停点粒度续玩快照,内容 owner=冒险结构);反面清单收窄到 meta 域+断点域豁免条款;写盘时机+停点快照/三结局清除;生产迁移链 v1→v2 补空;P1 快照预挂号兑现(rng `restore_adventure` 回填面);域界验收口径随改 |
|
||||
| v1.3 | 2026-10-01 | **版本兼容口径批(迭代 10 追加批 g 裁——用户拍板:版本兼容对齐 semantic version 标准)**:①版本策略成文口径——本域版本号语义 = **向后兼容的次序版本**:升版只允许「旧档在新代码可读可玩可续玩」(缺新键=合法空态、内容零丢失、断点可续),禁「新档在旧代码可读」承诺(向前兼容不做);②v2→v3 迁移体=空迁移(断点附随件扩列〔bonus_taken/bonus_pending/bonus_result/bonus_aux_carry/keystone_drops + bonus.ap 轴〕与摘要 additive 键 keystone_drops 均由读取方 .get 兜底——兼容性由冒烟 v1→v3 与 v2→v3 断点续玩断言承载,2026-10-01 增);③中断节点兼容义务:每升版必须在冒烟断言「旧版断点→新版续玩受理」(现行三档 v1/v2/v3 齐) |
|
||||
@@ -1,30 +1,30 @@
|
||||
# 节点奖励屏 UX spec(node-reward)
|
||||
|
||||
> 状态:v1.0(2026-09-29 迭代 7 T5 随批出生——06「节点奖励屏 UX spec」条目兑现;上游拍板=奖励节点三选一〔用户 2026-09-29 拍板②丙〕)
|
||||
> 形态沿革(2026-09-30 用户裁决甲):本屏承载的三选一投放系**切片期过渡形态**——终局=魔法道具局外同调件(设计权威底稿 design/research/proposal-magic-item-attunement-2026-09-22.md,八拍板 2026-09-22 在案),同调实施批立项后本屏随投放渠道退役/改写;过渡期各项现行拍板继续有效(口径正主与沿革详见[魔法道具](../systems/排雷循环/魔法道具.md)「定位与形态沿革」节)
|
||||
> 事实源:`game/presentation/views/node_overlay.gd`(show_reward_items/close_reward)+ `main.gd`(_reward_node_choices/_on_reward_item_chosen)
|
||||
> 相邻:[level-clear](level-clear.md)(关卡结算屏)/ [level-map](level-map.md)(关卡画面)/ [魔法道具](../systems/排雷循环/魔法道具.md)(内容权威)
|
||||
> 状态:v2.0(2026-10-01 迭代 10 追加批重写——用户拍板:奖励节点三选一与通关三选一**共享同池**;原「魔法道具投放渠道」过渡形态退役)
|
||||
> 事实源:`game/presentation/views/bonus_pick_overlay.gd`(三选一覆盖层共用面)+ `main.gd`(奖励节点分支——`adventure.node_bonus_candidates/apply_node_bonus`)+ `battle_narrator.gd`(node_bonus_header)
|
||||
> 相邻:[level-clear](level-clear.md)(通关三选一面——同一覆盖层的另一上下文)/ [level-map](level-map.md)(关卡画面)/ [魔法道具](../systems/排雷循环/魔法道具.md)(道具件内容权威)/ [冒险结构](../systems/冒险循环/冒险结构.md)(提升池单例与次数上限)
|
||||
|
||||
## 定位
|
||||
|
||||
奖励节点(reward node)到达即开的**模态构筑点**:从可用道具池取一件加入关卡背包。「节点奖励 = 功能道具的正式渠道」(关卡节点地图 v1.0 ⑤ 定稿)的呈现承载。
|
||||
奖励节点(reward node)到达即开的**提升三选一选择面**:与通关三选一共享同一提升池(六种:攻击/生命上限/护甲/GP 袋/魔法道具/行动点件)——同权重、同全冒险次数上限、同 reward_pick 封闭流。与通关面的差异仅在上下文:无部分回血、无钥石增量行,兑付后「继续」回图选路(不推进关卡)。
|
||||
|
||||
## 布局与交互
|
||||
|
||||
- 全屏变暗层 + 居中标题「关卡奖励点 / 取一件魔法道具(加入关卡背包——关卡结束清空)」。
|
||||
- 道具按钮横排三枚(150×72:字形大字=道具名首字 + 全名两行);**选择前不给出口**(继续按钮隐藏——强制三选一的构筑点语义)。
|
||||
- 池 ≤3 全列(现行有效道具三件:工兵令/探巢摆钟/爆破工兵包——选一即全列);池扩充后按注册序取前 3(呈现侧零 RNG 消费纪律)。
|
||||
- 池空(全未解锁/表空合法态)= 直通节点确认「此行无道具可取——继续前行」(切片期空态语义保留)。
|
||||
- 选择 → 道具入关卡域库存(来源归因 `reward_node`)→ toast「获得 X(入关卡背包)」→ C 区槽位刷新 → 兑付确认回选路(零自动推进同款)。
|
||||
- 载体 = 提升三选一覆盖层(BonusPickOverlay,与 [level-clear](level-clear.md) 共用组件——零新增交互原语)。
|
||||
- 头部行:「关卡奖励点 / 从提升池抽三选一(与通关三选一同池同权重同上限):选择一件带走:」(Narrator.node_bonus_header)。
|
||||
- 三卡点选(✓ 高亮,确认前可改选)→「确认选择」→ 兑付反馈(选中件效果 + 丢弃提示)→「继续」→ 回图选路(零自动推进同款)。
|
||||
- 魔法道具件满位:替换子面板(现有库存件 + 新件 N+1 选 N,丢旧收新 / 放弃新件两路——与通关面同构;丢弃目标 = 关卡域库存件句柄)。
|
||||
|
||||
## 口径
|
||||
|
||||
- 可用池 = aux 活行 ∩ 工坊解锁集(拍板③甲「解锁→进投放池」同源门控——未解锁不入奖励池与掉落池)。
|
||||
- 退役件(探宝罗盘)/失义件(显影尘/地穴全图卷)不入池;营地呈现「已退役 / 已解锁·能力封存中」(呈现 spec:[camp.md](camp.md) v1.0——2026-09-30 拍板点 4A 落位,原悬空指认解除)。
|
||||
- 库存语义=关卡域(跨战斗延续、关卡末清空+快照反馈);使用=挖掘期两步交互(C 区槽选中→棋盘目标格)。
|
||||
- **同池同上限**:件获取次数计数为冒险级单例——奖励节点与通关三选一两渠道合计判上限(达上限件在两渠道同样剔牌库);权重与牌库口径读冒险表(registry adventure_table 为准)。
|
||||
- 道具件入**本关库存**(即时可用,来源归因 `reward_node`)——区别于通关三选一的跨关携带(清库后授予、下一关生效)。
|
||||
- 「池空直通」合法态随共享池退役:池含无次数上限的 GP/道具件,牌库恒非空。
|
||||
- 退役件(探宝罗盘)/失义件不入道具件随机池(同调位约束口径不变,归[魔法道具](../systems/排雷循环/魔法道具.md))。
|
||||
|
||||
## 版本历史
|
||||
|
||||
| 版本 | 日期 | 改动 |
|
||||
|---|---|---|
|
||||
| v1.0 | 2026-09-29 | 迭代 7 T5 随批出生(三选一构筑点形态——用户拍板②丙) |
|
||||
| v2.0 | 2026-10-01 | **共享池改写(迭代 10 追加批——用户拍板「奖励节点与通关三选一相同、共享同一个池子」)**:①定位改提升三选一选择面(六种共享池、同权重同次数上限、同 reward_pick 流);②载体改 BonusPickOverlay 共用面(原 node_overlay 道具三选一面退役);③道具件满位升级为替换子交互两路(原过渡形态=满位不授予);④「池空直通」合法态退役(牌库恒非空);⑤道具件入本关库存(即时可用),区别于通关渠道的跨关携带 |
|
||||
@@ -17,8 +17,9 @@
|
||||
| 9 | 三选一面关游戏 → 主菜单「继续上局」 | 回到三选一面,候选与中断前逐位一致(不重抽);兑付后中断则回兑付反馈态 |
|
||||
| 10 | L3(末关)Boss 击杀 | 不出三选一面,直通冒险完成结算;钥石明细列逐杀三笔(首杀 3×3) |
|
||||
| 11 | 营地回城 | 钥石余额 = 本局逐杀掉落合计;营地呈现存量(与进账时刻不同义) |
|
||||
| 12 | 同调位替换子面板 | **过渡键 =1 时实机不可达**(carry 每关清空)——视觉验证随同调实施批 slots≥2 场景;本批以展示树探针/单测承载逻辑正确性 |
|
||||
| 12 | 同调位替换子面板 | **过渡键 =1 时通关渠道实机不可达**(carry 每关清空);**奖励节点渠道可达**(本关库存可满位——先取道具件再走奖励节点选道具件)→ 替换子面板 N+1 选 N、丢旧收新/放弃新件两路 |
|
||||
| 13 | 减动效开 | 三选一面静态化(无动画依赖),点选确认流程不变 |
|
||||
| 14 | 奖励节点三选一(追加批) | 奖励节点开与通关同款选择面(头部「关卡奖励点」、无回血/钥石行);点选兑付后「继续」回图选路;属性件计数与通关渠道合计判上限(取满 3 次后该件不再出现) |
|
||||
|
||||
## 二、代拍板追认清单(计划书 §10 白名单内——合并前逐条追认)
|
||||
|
||||
@@ -43,9 +44,20 @@
|
||||
| 熵增闸 | PASS 四查全过 | PASS |
|
||||
| MC | 改后锚 400 局(种 78000 both min)→ 通关率双窗读数报告(见 readings 报告) | 改前锚 400 局(种 77000) |
|
||||
|
||||
## 四、合并前待办(用户点名后执行)
|
||||
## 四、代拍板裁决记录(2026-10-01 用户逐条——首批 7 项 + 追加范围)
|
||||
|
||||
| # | 裁决 |
|
||||
|---|---|
|
||||
| a | **用备选**:复杀按关 2/2/3 递进(首杀 3 三关统一)——meta.tres/registry 已随改 |
|
||||
| b/c/d | 认同(件值 provisional / AP 每战首轮 +1 / 钥石行在战斗总结结算框) |
|
||||
| e | 说明已呈(同调位过渡形态白话)——**追加批已把奖励节点渠道升级为替换子交互两路**,原「满位不授予」收窄面随共享池消除;追认口径以 node-reward.md v2.0 / 冒险结构.md 同调位段现行文为准 |
|
||||
| f | 暂定维持 + 调研待办已登记 06(随机选取奖励对角色能力影响——首件 vs 权重随机双窗对照) |
|
||||
| g | semver 向后兼容口径成文(存档序列化.md v1.3)+ v2→v3 兼容断言随批(冒烟 ⑫) |
|
||||
| 追加 | **奖励节点三选一共享池**(用户指令)——设计/任务书/代码/文档/测试全落,见 `.zcode/plans/m3-iter10-addendum-task-2026-10-01.md` |
|
||||
|
||||
## 五、合并前待办(用户点名后执行)
|
||||
|
||||
1. 目检清单第一节实机执行(或先合并后目检,由用户裁)。
|
||||
2. 代拍板第二节逐条追认(或全按推荐走到底一句代总)。
|
||||
2. e 项白话说明后追认。
|
||||
3. 台账(active.md)/roadmap 迭代 10 行改写——按约定**推迟到合并批准时一并落籍**(分支上未动两共享文件)。
|
||||
4. MC 双窗读数报告判读(通关率下压方向是否符合预期带;AP 轴强度读数)→ 归曲线调参批联动。
|
||||
4. MC 三锚读数报告判读(含追加批共享池经济面变化)→ 归曲线调参批联动。
|
||||
@@ -620,7 +620,7 @@ entities:
|
||||
table: MetaTable(数据配置 承载——元进度实现批 2026-09-21 建表落籍;schema data/schemas/meta_table.gd + data/tables/meta.tres)
|
||||
kind: 元进度/永久强化数值行单点(定价 / Boss 两档 / 工坊阈值 / 换算率四行族——消费面一律走 ConfigDB.meta_* 访问器,禁直读行集)
|
||||
row_families: forge_products {attr ∈ {max_hp, atk, armor_class} 恰三, display_name, flat_value, base_price, price_ratio_pct ∈ [100,400]} · boss_keystone {level_index 连续从 1, first_kill ≥ repeat_kill} · workshop_items {item_id ∈ registry aux_items 六件词表恰全, display_name, clear_threshold ∈ [1,99]} · conversion {attr 与 forge 词表一致恰三, per_level_shift_pct ∈ [0,100]}
|
||||
provisional: 数值全 provisional 归元进度设计批配读数(骨架先行先例 = GP+商店房批)——现行落表值:flat 血+3/攻+1/甲+1、base 10 ratio 150 三行同;Boss 两档 **首杀 3 / 复杀 2 三关统一**(**迭代 10 奖励重构批重锚**——原 L1 5/1 · L2 8/2 · L3 12/3 退役;P-B 乙拍板贴近「Boss 掉 2–3 个」示例量级,子选项「复杀统一 2」归代拍板白名单待追认;精英掉 1 预留行随怪物批实装落);工坊阈值 1/1/2/2/3/3(按总通关次数);换算率全 0 占位
|
||||
provisional: 数值全 provisional 归元进度设计批配读数(骨架先行先例 = GP+商店房批)——现行落表值:flat 血+3/攻+1/甲+1、base 10 ratio 150 三行同;Boss 两档 **首杀 3 三关统一 / 复杀按关 2/2/3 递进**(**迭代 10 奖励重构批重锚 + 用户 2026-10-01 裁「a 用备选」**——原 L1 5/1 · L2 8/2 · L3 12/3 与代拍板「复杀统一 2」均退役;P-B 乙拍板贴近「Boss 掉 2–3 个」示例量级,L3 3/3 系 first≥repeat 结构约束的合法端点;精英掉 1 预留行随怪物批实装落);工坊阈值 1/1/2/2/3/3(按总通关次数);换算率全 0 占位
|
||||
impl: core/meta_progression.gd(MetaProgression 账本服务——余额/几何定价〔tier 级价格 = 迭代 floor(p × ratio/100)〕/购买/工坊解锁〔总通关次数渠道〕/settle_adventure Boss 两档入账/hero_override 注入计算;Core 层零 UI 零信号)
|
||||
workshop_unlock_rule: 首版只挂总通关次数渠道(Σ level_clear_counts 达阈值——2026-09-21 实现批代拍板 2 并经用户确认,GDD 未细分);**channel 可替换轴**(同日用户拍板:解锁条件须抽象、可替换、可扩展——渠道由 workshop_items.channel 字段声明〔缺省 total_clears,词表 MetaTable.WORKSHOP_CHANNELS 封闭〕,判定量换算单点 = MetaProgression._workshop_progress,换/加渠道 = 词表加值 + 一 match 分支,阈值比较与解锁写档零改动;未知渠道恒 0 = 永不解锁 fail-safe;候选 adventure_depth 最高通关深度 / keystone 直购双渠道已预留语义未实现);钥石直购价随定价批后补
|
||||
referenced_by: [元进度, 存档序列化, 数据配置]
|
||||
|
||||
@@ -80,6 +80,7 @@ var _bonus_pending := {} # 停点态:{"candidates": Array, "p
|
||||
var _bonus_result := {} # 已兑付待推进态(bonus_resolved 载荷副本——中断续玩不重兑付)
|
||||
var _bonus_aux_carry: Array[String] = [] # 道具件跨关携带(兑付后注入下一关库存——清库后授予的体验窗)
|
||||
var _keystone_drops: Array = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops——结算明细/复盘归因数据源)
|
||||
var _node_bonus_pending := {} # 奖励节点三选一停点态(迭代 10 追加批——共享池;断点不载:续玩重建到选路停点重走,恢复的计数器保证同候选重抽)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
@@ -197,6 +198,7 @@ func _reset_bonus_state() -> void:
|
||||
_bonus_result = {}
|
||||
_bonus_aux_carry = []
|
||||
_keystone_drops = []
|
||||
_node_bonus_pending = {}
|
||||
|
||||
|
||||
# TunePanel 百分比通道(测试面板覆盖——每关新 runner 都要应用;正式侧归元进度)
|
||||
@@ -331,6 +333,113 @@ func _finish_bonus_pick(picked: String, value: int, hp_before: int) -> void:
|
||||
_snapshot_breakpoint()
|
||||
|
||||
|
||||
# ---- 奖励节点三选一(迭代 10 追加批——用户拍板:与通关三选一共享同池同权重同次数上限)----
|
||||
|
||||
# 候选呈现(节点到达时调用——恰一 reward_pick scope 取数;本停点断点不载,
|
||||
# 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)
|
||||
func node_bonus_candidates() -> Array:
|
||||
return _draw_bonus_candidates()
|
||||
|
||||
|
||||
func present_node_bonus(candidates: Array) -> void:
|
||||
_node_bonus_pending = {"candidates": candidates.duplicate(true)}
|
||||
|
||||
|
||||
# 点选兑付(节点域——无部分回血无钥石;件获取计数与牌库剔除与通关三选一同账)
|
||||
func apply_node_bonus(id: String) -> bool:
|
||||
if _node_bonus_pending.is_empty():
|
||||
push_error("[冒险结构] apply_node_bonus 拒绝——无待选停点")
|
||||
return false
|
||||
var candidates: Array = _node_bonus_pending.get("candidates", []) as Array
|
||||
if not candidates.has(id):
|
||||
push_error("[冒险结构] apply_node_bonus 拒绝——件不在候选: %s" % id)
|
||||
return false
|
||||
var value: int = int(db.adventure.bonus_value(id))
|
||||
match id:
|
||||
"attack":
|
||||
bonus["atk"] = int(bonus.get("atk", 0)) + value
|
||||
_bonus_taken["attack"] = int(_bonus_taken.get("attack", 0)) + 1
|
||||
"max_hp":
|
||||
bonus["max_hp"] = int(bonus.get("max_hp", 0)) + value
|
||||
_bonus_taken["max_hp"] = int(_bonus_taken.get("max_hp", 0)) + 1
|
||||
"armor":
|
||||
bonus["armor_class"] = int(bonus.get("armor_class", 0)) + value
|
||||
_bonus_taken["armor"] = int(_bonus_taken.get("armor", 0)) + 1
|
||||
"ap":
|
||||
bonus["ap"] = int(bonus.get("ap", 0)) + value
|
||||
_bonus_taken["ap"] = int(_bonus_taken.get("ap", 0)) + 1
|
||||
"gp":
|
||||
if _runner != null:
|
||||
_runner.grant_bonus_gp(value)
|
||||
"aux_item":
|
||||
return _apply_node_aux(value)
|
||||
_:
|
||||
push_error("[冒险结构] apply_node_bonus 拒绝——未知件: %s" % id)
|
||||
return false
|
||||
_finish_node_bonus(id, value)
|
||||
return true
|
||||
|
||||
|
||||
# 节点渠道道具件(同调位约束):持有=关卡域库存+跨关携带,有位直接入本关库存(当关可用);
|
||||
# 满位转替换子交互(丢弃目标=库存件 handle——与通关渠道的 carry 序号分轨)
|
||||
func _apply_node_aux(count: int) -> bool:
|
||||
var pool: Array = _bonus_aux_pool()
|
||||
if pool.is_empty():
|
||||
push_error("[冒险结构] apply_node_bonus(aux_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)")
|
||||
return false
|
||||
var picked: Array[String] = []
|
||||
for i in maxi(1, count):
|
||||
picked.append(_roll_bonus_aux_item(pool))
|
||||
var slots := maxi(0, int(db.adventure.transitional_sync_slots))
|
||||
var records: Array = []
|
||||
if _runner != null and _runner.aux_inventory != null:
|
||||
records = _runner.aux_inventory.holding_records()
|
||||
var holding := records.size() + _bonus_aux_carry.size()
|
||||
if holding + picked.size() <= slots:
|
||||
if _runner != null:
|
||||
for iid in picked:
|
||||
_runner.grant_aux_item(iid, "reward_node")
|
||||
_finish_node_bonus("aux_item", picked.size())
|
||||
return true
|
||||
_node_bonus_pending["chosen"] = "aux_item"
|
||||
_node_bonus_pending["incoming"] = picked.duplicate()
|
||||
aux_replace_presented.emit(records, {"handle": -1, "item_id": String(picked[0]), "count": picked.size()})
|
||||
return true
|
||||
|
||||
|
||||
func _resolve_node_replace(discard_handle: int) -> bool:
|
||||
if _node_bonus_pending.is_empty() or String(_node_bonus_pending.get("chosen", "")) != "aux_item":
|
||||
push_error("[冒险结构] resolve_node_replace 拒绝——无替换在途")
|
||||
return false
|
||||
var incoming: Array = _node_bonus_pending.get("incoming", []) as Array
|
||||
if discard_handle >= 0:
|
||||
if _runner == null or _runner.aux_inventory == null or not _runner.aux_inventory.discard_item(discard_handle):
|
||||
push_error("[冒险结构] resolve_node_replace 拒绝——句柄不在持有集: %d" % discard_handle)
|
||||
return false
|
||||
for iid in incoming:
|
||||
_runner.grant_aux_item(String(iid), "reward_node")
|
||||
# 放弃新件(discard_handle < 0)= 原持有集不动、新件不获取
|
||||
_finish_node_bonus("aux_item", incoming.size())
|
||||
return true
|
||||
|
||||
|
||||
# 替换受理分发(单入口——节点停点在途优先按库存句柄,否则按通关停点的携带序号)
|
||||
func resolve_bonus_replace(discard: int) -> bool:
|
||||
if not _node_bonus_pending.is_empty() and _node_bonus_pending.has("incoming"):
|
||||
return _resolve_node_replace(discard)
|
||||
return resolve_aux_replace(discard)
|
||||
|
||||
|
||||
func _finish_node_bonus(picked: String, value: int) -> void:
|
||||
_node_bonus_pending = {}
|
||||
bonus_resolved.emit({
|
||||
"context": "reward_node",
|
||||
"picked": picked,
|
||||
"value": value,
|
||||
"level_index": level_index,
|
||||
})
|
||||
|
||||
|
||||
# 加权洗牌不放回掷 3(E-S weight-key:每件 key = U(0,1)^(1/权重) 取前 3——单次零重复、逐件按权重出
|
||||
# 概率;达次数上限件先剔牌库〔AP 用尽 2 次/属性件达 3 次永不再现;GP/道具件无次数上限,牌库恒非空〕;
|
||||
# 道具件解锁池空集〔白板/门控态〕不入牌库。消费 reward_pick 封闭流——一次停点恰一 scope,同种子同三候选)
|
||||
|
||||
@@ -73,6 +73,24 @@ func holding_count() -> int:
|
||||
return n
|
||||
|
||||
|
||||
# 持有件记录(迭代 10 追加批——奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标)
|
||||
func holding_records() -> Array:
|
||||
var out: Array = []
|
||||
for rec in _items:
|
||||
if not bool(rec.get("used", false)):
|
||||
out.append({"handle": int(rec.get("handle", 0)), "item_id": String(rec.get("item_id", ""))})
|
||||
return out
|
||||
|
||||
|
||||
# 丢弃(同调位替换交互节点渠道——腾位收新;与 Used 消费分轨的一次性移除,快照不再含)
|
||||
func discard_item(handle: int) -> bool:
|
||||
for i in _items.size():
|
||||
if int(_items[i].get("handle", -1)) == handle and not bool(_items[i].get("used", false)):
|
||||
_items.remove_at(i)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# 板级零信息可用性预判(图标置灰/入口禁用判据——GDD「整张图都没东西可看」级):
|
||||
# 真视恒可用(零雷窗口=信息非空);AP 类恒可用(组合判据取 OR——任一效果有信息即可用);
|
||||
# 纯预读行 = 板上还有未开格才可用。
|
||||
|
||||
@@ -34,7 +34,7 @@ boss_keystone = [{
|
||||
}, {
|
||||
"level_index": 3,
|
||||
"first_kill": 3,
|
||||
"repeat_kill": 2
|
||||
"repeat_kill": 3
|
||||
}]
|
||||
workshop_items = [{
|
||||
"item_id": "probe_dust",
|
||||
@@ -101,8 +101,9 @@ conversion = [{
|
||||
; 零消费——_defender_ac 主角分支本次接线,缺参/-1 = 表值兜底);修复后甲轴读数 +1.2±0.4 为
|
||||
; 解锁后参数实测,非基线外推。战役轨迹三花费序(生命/攻击/护甲优先)随批采集验证。
|
||||
;
|
||||
; 迭代 10 奖励重构批(2026-10-01——boss_keystone 重锚,P-B 乙拍板):三关统一 首杀 3 / 复杀 2
|
||||
; (原 5/1 · 8/2 · 12/3 退役)——贴近「Boss 掉 2–3 个钥石」用户示例量级、保持 first_kill ≥ repeat_kill
|
||||
; 结构;子选项「复杀统一 2」归代拍板白名单(另一子选项 = 按关 2/2/3 递进)。钥石渠道自本批起 =
|
||||
; 迭代 10 奖励重构批(2026-10-01——boss_keystone 重锚,P-B 乙拍板):首杀 3 三关统一;
|
||||
; 复杀按关 2/2/3 递进(**用户 2026-10-01 裁「a 用备选」**——原代拍板「复杀统一 2」被推翻;
|
||||
; L3 首杀=复杀=3 系 first_kill ≥ repeat_kill 结构约束下的合法端点)。
|
||||
; 钥石渠道自本批起 =
|
||||
; Boss/精英击杀逐杀掉落(大奖励 keystone_per_level 渠道退役;精英掉 1 预留行随怪物批实装落)。
|
||||
; 产出结构变化 → 元进度标定义务重开(06-待办清单 2026-10-01 登记条)。provisional 归曲线调参批。
|
||||
@@ -121,6 +121,11 @@ static func bonus_keystone_line(payload: Dictionary) -> String:
|
||||
return "钥石 +%d · 累计 %d" % [int(payload.get("keystone_gained", 0)), int(payload.get("keystone_total", 0))]
|
||||
|
||||
|
||||
# 奖励节点三选一头部行(迭代 10 追加批——与通关三选一共享同池;node-reward.md v2)
|
||||
static func node_bonus_header() -> String:
|
||||
return "关卡奖励点\n从提升池抽三选一(与通关三选一同池同权重同上限):\n选择一件带走:"
|
||||
|
||||
|
||||
# 冒险结算屏文案(摘要五件事:打穿没有 / 通关几关 / 止步第几关 / 钥石合计 / 主种子)
|
||||
static func adventure_summary_text(summary: Dictionary) -> String:
|
||||
var verdict := "冒险失败 · 未打穿"
|
||||
|
||||
+21
-47
@@ -45,7 +45,8 @@ var _settlement_stop_pending := false # 卡⑥ 战斗胜利总结结算框挂起
|
||||
var _pending_candidates: Array = [] # 结算框挂起期的选路候选暂存(choices_presented 载荷)
|
||||
var _last_gain_amount := 0 # 最近一笔钱包入账(卡⑥ GP 增量取数面——wallet_changed gain 载荷)
|
||||
var _pending_bonus := {} # 三选一停点载荷暂存(迭代 10——战斗总结结算确认后开三选一面;续玩停点复用)
|
||||
var bonus_overlay: BonusPickOverlay # 提升三选一覆盖层(迭代 10——level-clear.md v2 选择面承载)
|
||||
var bonus_overlay: BonusPickOverlay # 提升三选一覆盖层(迭代 10——level-clear.md v2 选择面承载;追加批起奖励节点共用)
|
||||
var _bonus_context := "level_clear" # 三选一面上下文(level_clear=通关停点→推进下一关 / reward_node=奖励节点→回图选路)
|
||||
var _start_overlay: Control
|
||||
var _end_overlay: BattleEndOverlay
|
||||
var _inter_overlay: Control
|
||||
@@ -174,7 +175,6 @@ func _build_ui() -> void:
|
||||
legend_overlay.closed.connect(_on_modal_closed.bind(legend_overlay))
|
||||
review_overlay.closed.connect(_on_modal_closed.bind(review_overlay))
|
||||
node_overlay.continue_pressed.connect(_on_node_continue)
|
||||
# 迭代 7 T5——item_chosen 连接在奖励分支首唤时防重接线(上方)
|
||||
level_screen.route_chosen.connect(_on_route_chosen)
|
||||
level_screen.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119)
|
||||
shop_overlay.buy_requested.connect(_on_shop_buy)
|
||||
@@ -394,10 +394,13 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
|
||||
_show_board_fx_for_node(t)
|
||||
log_view.append_bb(Narrator.node_banner(runner, node, t))
|
||||
elif t == "reward":
|
||||
_open_modal(node_overlay)
|
||||
node_overlay.item_chosen.connect(_on_reward_item_chosen) if not node_overlay.item_chosen.is_connected(_on_reward_item_chosen) else null
|
||||
node_overlay.show_reward_items(_reward_node_choices())
|
||||
log_view.append_bb(Narrator.reward_line())
|
||||
# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;BonusPickOverlay 共用面,
|
||||
# 道具件满位替换子交互同构)。原「魔法道具投放渠道」三选一(迭代 7 过渡形态)随批退役。
|
||||
_bonus_context = "reward_node"
|
||||
var node_cands: Array = adventure.node_bonus_candidates()
|
||||
_open_modal(bonus_overlay)
|
||||
bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(node_cands))
|
||||
adventure.present_node_bonus(node_cands)
|
||||
log_view.append_bb(Narrator.reward_line())
|
||||
elif t == "rest":
|
||||
var healed: int = int(info.get("healed", 0))
|
||||
@@ -422,6 +425,7 @@ func _on_bonus_presented(payload: Dictionary) -> void:
|
||||
if int(payload.get("aux_cleared", 0)) > 0:
|
||||
board_zone.toast.show_toast("上关剩余 %d 件魔法道具已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 迭代 7 T4 清空反馈
|
||||
_pending_bonus = payload.duplicate(true)
|
||||
_bonus_context = "level_clear"
|
||||
if bool(payload.get("replace_pending", false)):
|
||||
# 续玩:替换子交互在途中断——重建三选一面后直落替换子态(候选不重抽)
|
||||
_show_bonus_pick()
|
||||
@@ -452,7 +456,10 @@ func _bonus_card_texts(candidates: Array) -> Array:
|
||||
|
||||
|
||||
func _on_bonus_pick_confirmed(item_id: String) -> void:
|
||||
if adventure == null or not adventure.pick_bonus(item_id):
|
||||
if adventure == null:
|
||||
return
|
||||
var ok: bool = adventure.apply_node_bonus(item_id) if _bonus_context == "reward_node" else adventure.pick_bonus(item_id)
|
||||
if not ok:
|
||||
push_error("[组合根] 三选一兑付被拒——见输出日志")
|
||||
|
||||
|
||||
@@ -474,8 +481,8 @@ func _open_bonus_replace(items: Array, incoming: Dictionary) -> void:
|
||||
named.size(), int(db.adventure.transitional_sync_slots))
|
||||
|
||||
|
||||
func _on_bonus_replace_chosen(discard_index: int) -> void:
|
||||
if adventure == null or not adventure.resolve_aux_replace(discard_index):
|
||||
func _on_bonus_replace_chosen(discard: int) -> void:
|
||||
if adventure == null or not adventure.resolve_bonus_replace(discard):
|
||||
push_error("[组合根] 同调位替换受理被拒——见输出日志")
|
||||
|
||||
|
||||
@@ -486,10 +493,14 @@ func _on_bonus_resolved(result: Dictionary) -> void:
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
# 继续钮:收三选一面 → 推进下一关(零自动推进——兑付后受理)
|
||||
# 继续钮:收三选一面 → 上下文分派(通关停点=推进下一关 / 奖励节点=兑付确认回图选路——零自动推进)
|
||||
func _on_bonus_continue() -> void:
|
||||
_close_modal(bonus_overlay)
|
||||
bonus_overlay.close_pick()
|
||||
if _bonus_context == "reward_node":
|
||||
if runner != null:
|
||||
runner.continue_after_node()
|
||||
return
|
||||
if _adventure_mode and adventure != null:
|
||||
if not adventure.continue_to_next_level():
|
||||
_fatal_screen("关卡推进失败——见输出日志")
|
||||
@@ -1039,43 +1050,6 @@ func _aux_unlocked_set() -> Dictionary:
|
||||
return out
|
||||
|
||||
|
||||
|
||||
# 摇取面:活行 ∩ 工坊解锁集(拍板③甲——解锁进投放池同源口径);池 ≤3 全列(当前三件=选一即全列),
|
||||
# 池扩充后加权摇三(确定性摇取:读 seed 流外——呈现侧零 RNG 消费纪律 → 全列前 3 按注册序)。
|
||||
func _reward_node_choices() -> Array:
|
||||
if runner == null or runner.aux_inventory == null:
|
||||
return []
|
||||
var out: Array = []
|
||||
for row in db.aux_active_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if not _aux_unlocked.has(iid):
|
||||
continue # 未解锁不入奖励池(与掉落投放同门控)
|
||||
out.append({
|
||||
"item_id": iid,
|
||||
"display_name": String(row.get("display_name", "")),
|
||||
"glyph": String(row.get("display_name", "?")).left(1),
|
||||
})
|
||||
if out.size() >= 3:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
# 选择兑付:入关卡域库存(来源归因 reward_node)→ 收屏 → 兑付确认回选路(零自动推进同款)
|
||||
func _on_reward_item_chosen(item_id: String) -> void:
|
||||
if runner == null or runner.aux_inventory == null:
|
||||
return
|
||||
var rec: Dictionary = runner.grant_aux_item(item_id, "reward_node")
|
||||
if rec.is_empty():
|
||||
# 同调位满位拒绝(迭代 10 过渡承载——奖励节点渠道满位不授予;替换交互归三选一渠道,
|
||||
# 全渠道统一收口随同调实施批)。覆盖层保持开、零入账零关闭(玩家可改选继续/离开)
|
||||
board_zone.toast.show_toast("同调位已满——「%s」未获取" % String(db.aux_item_row(item_id).get("display_name", item_id)), "warn")
|
||||
return
|
||||
node_overlay.close_reward()
|
||||
_refresh_aux_slots()
|
||||
board_zone.toast.show_toast("获得 %s(入关卡背包)" % String(db.aux_item_row(item_id).get("display_name", item_id)), "info")
|
||||
_on_node_continue()
|
||||
|
||||
|
||||
# 槽点击(第一步):AP 类(target=none)点击即用(无目标);空间类进/出选中态(toggle 取消)。
|
||||
func _on_aux_slot_clicked(slot_handle: int) -> void:
|
||||
if settlement == null or runner == null or runner.aux_inventory == null:
|
||||
|
||||
@@ -127,9 +127,9 @@ func open_replace(items: Array, incoming_name: String, holding: int, slots: int)
|
||||
for c in _replace_box.get_children():
|
||||
c.queue_free()
|
||||
for it in items:
|
||||
var idx := int(it.get("index", -1))
|
||||
var tag: int = int(it.get("index", it.get("handle", -1))) # 通关渠道=携带序号 / 节点渠道=库存句柄(核心分发面裁决)
|
||||
var btn := UiKit.button("丢弃「%s」换新" % String(it.get("display_name", it.get("item_id", ""))))
|
||||
btn.pressed.connect(func(): replace_chosen.emit(idx))
|
||||
btn.pressed.connect(func(): replace_chosen.emit(tag))
|
||||
_replace_box.add_child(btn)
|
||||
var decline := UiKit.button("放弃新件(不获取)")
|
||||
decline.pressed.connect(func(): replace_chosen.emit(-1))
|
||||
|
||||
@@ -6,11 +6,9 @@ class_name NodeOverlay
|
||||
extends Control
|
||||
|
||||
signal continue_pressed
|
||||
signal item_chosen(item_id: String) # 迭代 7 T5——奖励节点三选一兑付(拍板②丙:奖励屏=小构筑点)
|
||||
|
||||
var _label: Label
|
||||
var _btn: Button
|
||||
var _choice_box: HBoxContainer # 道具三选一按钮区(show_reward_items 时呈现)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
@@ -30,56 +28,16 @@ func _init() -> void:
|
||||
box.add_child(_label)
|
||||
_btn = UiKit.button("继续")
|
||||
_btn.pressed.connect(_emit_continue_pressed)
|
||||
_choice_box = HBoxContainer.new()
|
||||
_choice_box.add_theme_constant_override("separation", 12)
|
||||
_choice_box.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
box.add_child(_choice_box)
|
||||
box.add_child(_btn)
|
||||
visible = false
|
||||
|
||||
|
||||
# 奖励节点兑付(迭代 7 T5——「节点奖励=功能道具正式渠道」关卡节点地图.md:110 定稿兑现;
|
||||
# 拍板②丙=三选一呈现:池 ≤3 全列(当前有效道具三件——选一即全列),池扩充后加权摇三)。
|
||||
# choices = [{item_id, display_name, glyph}](组合根组装——视图纯呈现,选择经信号外发)。
|
||||
# 选择前「继续」隐藏(强制选择);选择即入库存+屏收。
|
||||
func show_reward_items(choices: Array) -> void:
|
||||
for c in _choice_box.get_children():
|
||||
c.queue_free()
|
||||
if choices.is_empty():
|
||||
# 池空(全未解锁/表空合法态)——直通节点确认(切片期合法空态语义保留)
|
||||
_label.text = "关卡奖励点
|
||||
(此行无道具可取——继续前行)"
|
||||
_btn.text = "继续"
|
||||
_btn.visible = true
|
||||
_choice_box.visible = false
|
||||
visible = true
|
||||
return
|
||||
_label.text = "关卡奖励点
|
||||
取一件魔法道具(加入关卡背包——关卡结束清空)"
|
||||
for ch in choices:
|
||||
var cid := String(ch.get("item_id", ""))
|
||||
var btn := UiKit.button("%s %s" % [String(ch.get("glyph", "?")), String(ch.get("display_name", ""))])
|
||||
btn.custom_minimum_size = Vector2(150, 72)
|
||||
btn.pressed.connect(func(): item_chosen.emit(cid))
|
||||
_choice_box.add_child(btn)
|
||||
_btn.visible = false # 选择前不给出口(强制三选一——构筑点语义)
|
||||
_choice_box.visible = true
|
||||
visible = true
|
||||
|
||||
|
||||
func show_rest(healed: int, hp_after: int, max_hp: int) -> void:
|
||||
_label.text = "血量恢复点\n休息回复 %d HP(%d → %d / %d)" % [
|
||||
healed, hp_after - healed, hp_after, max_hp]
|
||||
_btn.text = "休息完毕,继续"
|
||||
visible = true
|
||||
|
||||
|
||||
# 选择完成收屏(组合根在 item_chosen 处理毕调——入库存+确认后回选路由同拍)
|
||||
func close_reward() -> void:
|
||||
_choice_box.visible = false
|
||||
_btn.visible = true
|
||||
visible = false
|
||||
|
||||
# 探针复用:以真实按钮路径触发继续
|
||||
func press_continue() -> void:
|
||||
_btn.pressed.emit()
|
||||
|
||||
@@ -172,7 +172,19 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
|
||||
st.boss_depth = int(node.get("node_depth", -1))
|
||||
st.hp_at_boss = int(adr.ledger["hero_hp"])
|
||||
elif t == "reward":
|
||||
r.continue_after_node() # 奖励点 = 内容待拍板直通节点(格面资源批 2026-09-15)
|
||||
# 奖励节点三选一(迭代 10 追加批——共享池确定性策略:候选取首件、替换放弃新件;
|
||||
# 兑付后 continue_after_node 回图选路)
|
||||
var ncands: Array = adr.node_bonus_candidates()
|
||||
st.reward_events.append({"node_bonus_pick": ncands.duplicate(true)})
|
||||
if ncands.is_empty():
|
||||
r.continue_after_node() # 牌库空合法态直通(结构上不可达——gp/道具件无上限,防御保留)
|
||||
else:
|
||||
adr.present_node_bonus(ncands)
|
||||
if adr.apply_node_bonus(String(ncands[0])):
|
||||
if not adr._node_bonus_pending.is_empty() and adr._node_bonus_pending.has("incoming"):
|
||||
adr.resolve_bonus_replace(-1) # 满位替换:放弃新件
|
||||
else:
|
||||
st.result = "advance_failed"
|
||||
elif t == "rest":
|
||||
st.rest_healed = int(info.get("healed", 0))
|
||||
st.hp_before_rest = int(info.get("hp", 0)) - st.rest_healed
|
||||
@@ -201,17 +213,22 @@ static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> voi
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
# 三选一兑付完成 → 推进下一关(自动路径无停点等待——零自动推进契约由显式确认满足)
|
||||
static func _on_adventure_bonus_resolved(result: Dictionary, st: Dictionary, adr) -> void:
|
||||
st.reward_events.append({"bonus_resolved": result.duplicate(true)})
|
||||
if not adr.continue_to_next_level():
|
||||
# 同调位满位替换子交互(确定性策略:放弃新件不获取——分发面按停点上下文裁决节点/通关渠道)
|
||||
static func _on_adventure_aux_replace(items: Array, incoming: Dictionary, st: Dictionary, adr) -> void:
|
||||
st.reward_events.append({"aux_replace": incoming.duplicate(true)})
|
||||
if not adr.resolve_bonus_replace(-1):
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
# 同调位满位替换子交互(确定性策略:放弃新件不获取——-1)
|
||||
static func _on_adventure_aux_replace(items: Array, incoming: Dictionary, st: Dictionary, adr) -> void:
|
||||
st.reward_events.append({"aux_replace": incoming.duplicate(true)})
|
||||
if not adr.resolve_aux_replace(-1):
|
||||
# 三选一兑付完成 → 上下文分派(通关停点=推进下一关 / 奖励节点=回图选路)
|
||||
static func _on_adventure_bonus_resolved(result: Dictionary, st: Dictionary, adr) -> void:
|
||||
st.reward_events.append({"bonus_resolved": result.duplicate(true)})
|
||||
if String(result.get("context", "")) == "reward_node":
|
||||
var r = adr.runner()
|
||||
if r == null or not r.continue_after_node():
|
||||
st.result = "advance_failed"
|
||||
return
|
||||
if not adr.continue_to_next_level():
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
|
||||
+59
-8
@@ -305,17 +305,30 @@ func _smoke_adventure() -> void:
|
||||
check(int(sum.get("master_seed", -1)) == 42, "摘要携带主种子(42)")
|
||||
var picks: Array = []
|
||||
var resolves: Array = []
|
||||
var node_picks := 0
|
||||
for ev in (st["reward_events"] as Array):
|
||||
if (ev as Dictionary).has("bonus_pick"):
|
||||
picks.append((ev as Dictionary)["bonus_pick"])
|
||||
elif (ev as Dictionary).has("node_bonus_pick"):
|
||||
node_picks += 1 # 奖励节点三选一(追加批——每关奖励节点各 1 次)
|
||||
elif (ev as Dictionary).has("bonus_resolved"):
|
||||
resolves.append((ev as Dictionary)["bonus_resolved"])
|
||||
check(picks.size() == 2 and resolves.size() == 2,
|
||||
"三选一恰呈现 2 次、恰兑付 2 次(L1/L2 后——末关不发,奖励只朝下一战生效)")
|
||||
check(picks.size() == 2 and resolves.size() == 2 + node_picks,
|
||||
"通关三选一恰呈现 2 次、恰兑付 2 次(L1/L2 后——末关不发;resolves 含节点回执另计)")
|
||||
check(node_picks >= 1, "奖励节点三选一 ≥1 次(L1 链必经 1 个;L2/L3 图可选路经若干——实际经停 %d)" % node_picks)
|
||||
var node_resolves := 0
|
||||
for r0 in resolves:
|
||||
if String((r0 as Dictionary).get("context", "")) == "reward_node":
|
||||
node_resolves += 1
|
||||
check(node_resolves == node_picks, "奖励节点兑付回执与经停逐笔相等(context=reward_node——同账计数 %d/%d)" % [node_resolves, node_picks])
|
||||
var level_resolves: Array = []
|
||||
for r0 in resolves:
|
||||
if String((r0 as Dictionary).get("context", "")) != "reward_node":
|
||||
level_resolves.append(r0)
|
||||
var heal_ok := picks.size() == 2
|
||||
var cand_ok := picks.size() == 2
|
||||
var ks_gain_ok := picks.size() == 2
|
||||
var resolve_ok := resolves.size() == 2
|
||||
var resolve_ok := level_resolves.size() == 2
|
||||
var max_flat := int(_db.adventure.bonus_max_hp_flat)
|
||||
var heal_pct := int(_db.adventure.clear_heal_pct)
|
||||
var hp_acc := 0 # max_hp 件累加(回血封顶断言的生效上限推导)
|
||||
@@ -342,7 +355,7 @@ func _smoke_adventure() -> void:
|
||||
or int(p.get("keystone_total", -1)) != ks_prefix:
|
||||
ks_gain_ok = false
|
||||
if resolve_ok:
|
||||
var r: Dictionary = resolves[i]
|
||||
var r: Dictionary = level_resolves[i]
|
||||
if not (p.get("candidates", []) as Array).has(String(r.get("picked", ""))) \
|
||||
or int(r.get("value", -1)) != int(_db.adventure.bonus_value(String(r.get("picked", "")))):
|
||||
resolve_ok = false
|
||||
@@ -356,14 +369,14 @@ func _smoke_adventure() -> void:
|
||||
var k := 5.0 # hp+400 ⇒ TunePanel 百分比后乘 ×5(加算先加、百分比后乘——生效次序口径)
|
||||
var exp_base := maxi(1, int(round(float(int(_db.hero.max_hp)) * k)))
|
||||
var valid_hp := {}
|
||||
for acc in 3:
|
||||
for acc in 4:
|
||||
valid_hp[maxi(1, int(round(float(int(_db.hero.max_hp) + acc * max_flat) * k)))] = true
|
||||
var seq_ok := seq.size() > 0 and int(seq[0]) == exp_base
|
||||
for v in (seq as Array):
|
||||
if not valid_hp.has(int(v)):
|
||||
seq_ok = false
|
||||
check(seq_ok,
|
||||
"各战上限入参 ∈ {基础, 基础+件值, 基础+2×件值}(覆盖值通道传播——首战=基础表值 %d;AP 件不加上限)" % exp_base)
|
||||
"各战上限入参 ∈ {基础..基础+3×件值}(覆盖值通道——通关+奖励节点双渠道共享上限 3;首战=基础 %d)" % exp_base)
|
||||
# ③ 种子域可复现(同种子两局:summary + 上限序列 + 行进序列 + 三选一停点载荷逐位一致)
|
||||
var st2: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
|
||||
check(st2["summary"] == sum and st2["encounter_max_hp_seq"] == seq
|
||||
@@ -533,6 +546,19 @@ func _smoke_adventure() -> void:
|
||||
a12._on_level_finished({"result": "victory", "hp": max_full, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
check(int(cap["payload"].get("heal_gained", -1)) == 0,
|
||||
"满血通关回血 0 合法(封顶口径——heal_gained=0 非负溢出)")
|
||||
# ⑬ 奖励节点共享池(迭代 10 追加批——同 _draw 同上限同计数;GP 件走同一入账收口)
|
||||
var a13 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(13))
|
||||
check(a13.start_adventure(13, {"first_script": true, "aux_unlocked": {"blast_kit": true}}),
|
||||
"节点共享池场景:冒险启动(seed 13)")
|
||||
a13._bonus_taken["attack"] = int(_db.adventure.bonus_cap_attack)
|
||||
var ndraw: Array = a13.node_bonus_candidates()
|
||||
check(not (ndraw as Array).has("attack"),
|
||||
"奖励节点共享池:达上限件同样剔牌库(同 _draw 同上限——%s)" % str(ndraw))
|
||||
a13.present_node_bonus(["gp"])
|
||||
var gp13 := int(a13.ledger.get("gp", 0))
|
||||
check(a13.apply_node_bonus("gp") and int(a13.ledger.get("gp", 0)) == gp13 + int(_db.adventure.bonus_gp_amount),
|
||||
"奖励节点 GP 件入账(钱包「奖励入账」收口——+%d)" % int(_db.adventure.bonus_gp_amount))
|
||||
check(a13._node_bonus_pending.is_empty(), "节点停点态兑付后清零(恰一次语义同构)")
|
||||
var s10: Dictionary = a10.summary
|
||||
check(String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)),
|
||||
"放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)")
|
||||
@@ -562,7 +588,14 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
|
||||
r.pick_next_node(int(ids[0]))
|
||||
continue
|
||||
var t := String(r.current_node().get("type", ""))
|
||||
if t == "reward" or t == "rest":
|
||||
if t == "reward":
|
||||
# 奖励节点三选一(迭代 10 追加批——候选取首件、替换放弃新件、兑付确认回图)
|
||||
var ncands: Array = a.node_bonus_candidates()
|
||||
if not ncands.is_empty() and a.apply_node_bonus(String(ncands[0])):
|
||||
if not a._node_bonus_pending.is_empty() and a._node_bonus_pending.has("incoming"):
|
||||
a.resolve_bonus_replace(-1)
|
||||
r.continue_after_node()
|
||||
elif t == "rest":
|
||||
r.continue_after_node()
|
||||
return false
|
||||
|
||||
@@ -3632,7 +3665,8 @@ func _smoke_save() -> void:
|
||||
"user://smoke_save_spend.json", "user://smoke_save_highver.json",
|
||||
"user://smoke_save_corrupt.json", "user://smoke_save_migrate.json",
|
||||
"user://smoke_save_domain.json", "user://smoke_save_cap.json",
|
||||
"user://smoke_save_integration.json", "user://smoke_save_breakpoint.json"]
|
||||
"user://smoke_save_integration.json", "user://smoke_save_breakpoint.json",
|
||||
"user://smoke_save_v2compat.json"]
|
||||
var stale: Array = []
|
||||
for p in paths:
|
||||
stale.append(p)
|
||||
@@ -3816,6 +3850,23 @@ func _smoke_save() -> void:
|
||||
check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 3,
|
||||
"v1 旧档迁移后写盘 version == 3")
|
||||
|
||||
# ⑫ v2 旧档→v3 兼容(迭代 10 g 裁——semver 向后兼容:旧断点零丢失、新代码可续玩)
|
||||
var fv2 = sm_script.new(paths[10])
|
||||
var fv2f := FileAccess.open(paths[10], FileAccess.WRITE)
|
||||
fv2f.store_string(JSON.stringify({"version": 2, "meta": {"keystone_balance": 7},
|
||||
"breakpoint": {"level_index": 1, "node_id": -1, "hero_hp": 40, "gp": 5,
|
||||
"bonus": {"atk": 0, "max_hp": 0, "armor_class": 0}, "shop_purchased": {},
|
||||
"template_pick": -1, "master_seed": 777, "rng_counter": 3, "keystone": 0,
|
||||
"visited_ids": []}}))
|
||||
fv2f.close()
|
||||
check(fv2.load_from_disk() and fv2.has_breakpoint() and fv2.keystone_balance() == 7,
|
||||
"v2 旧档迁移 v3:断点域与 meta 域原样保留(缺新附随件=合法空态)")
|
||||
check(int((fv2.breakpoint_snapshot() as Dictionary).get("master_seed", 0)) == 777,
|
||||
"v2 旧断点字段零丢失(向后兼容——breakpoint 内容不因 schema 升版重写)")
|
||||
var adr_v2 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(777))
|
||||
check(adr_v2.resume_adventure(fv2.breakpoint_snapshot(), {"hero_override": {}}),
|
||||
"v2 旧断点续玩受理(缺 bonus 附随件走常规重建路径——g 裁 semver 口径)")
|
||||
|
||||
# 段尾清理(测试文件绝不残留)
|
||||
for p in stale:
|
||||
if FileAccess.file_exists(p):
|
||||
|
||||
@@ -1,41 +1,75 @@
|
||||
extends SceneTree
|
||||
# 奖励节点三选一 UI 直验套件(迭代 7 T5-hotfix 转正——奖励节点 UI 无探针路径触达的盲区补漏:
|
||||
# show_reward_items 函数体曾在合并中丢失而全闸绿灯实机炸——「功能存在 ≠ 功能可达」的测试面教训)。
|
||||
# 三选一覆盖层 UI 直验套件(迭代 7 T5-hotfix 转正;迭代 10 追加批随面迁移改写)——
|
||||
# 原守 node_overlay.show_reward_items;奖励节点三选一与通关三选一合并共用 BonusPickOverlay 后,
|
||||
# 本套件改守共用面的「功能存在 ≠ 功能可达」盲区:选择外发/强制确认两段式/替换子面板两路/结果态。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/reward_overlay_test.gd(expect 0)
|
||||
# 覆盖:池空直通态 / 全列呈现按钮数 / 强制选择(继续隐藏)/ item_chosen 信号外发。
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
func _run() -> void:
|
||||
var ov = load("res://presentation/views/node_overlay.gd").new()
|
||||
var ov = load("res://presentation/views/bonus_pick_overlay.gd").new()
|
||||
root.add_child(ov)
|
||||
var called := []
|
||||
ov.item_chosen.connect(func(id: String): called.append(id))
|
||||
ov.show_reward_items([])
|
||||
if ov.visible != true or ov._btn.visible != true:
|
||||
print("[reward_verify] FAIL — 池空态应为直通确认(visible/继续可见)")
|
||||
quit(1)
|
||||
return
|
||||
ov.close_reward()
|
||||
ov.show_reward_items([
|
||||
{"item_id": "sapper_order", "display_name": "工兵令", "glyph": "工"},
|
||||
{"item_id": "nest_pendulum", "display_name": "探巢摆钟", "glyph": "摆"},
|
||||
var picked := []
|
||||
var replaced := []
|
||||
var continued := []
|
||||
ov.pick_confirmed.connect(func(id: String): picked.append(id))
|
||||
ov.replace_chosen.connect(func(tag: int): replaced.append(tag))
|
||||
ov.continue_pressed.connect(func(): continued.append(true))
|
||||
# ① 选择态:三卡呈现、确认钮隐藏(两段式——点选前不给确认)
|
||||
ov.open_pick("第 1 层通过!", [
|
||||
{"id": "attack", "text": "攻击力提升\n攻击 +1"},
|
||||
{"id": "gp", "text": "金币袋\n金币 +20"},
|
||||
{"id": "ap", "text": "行动点提升\n每战首轮 AP +1"},
|
||||
])
|
||||
var btns := 0
|
||||
for c in ov._choice_box.get_children():
|
||||
if c is Button:
|
||||
btns += 1
|
||||
if btns != 2 or ov._btn.visible:
|
||||
print("[reward_verify] FAIL — 两件应呈现两按钮且继续隐藏(btns=%d 继续可见=%s)" % [btns, str(ov._btn.visible)])
|
||||
if ov.visible != true or btns != 3 or ov._confirm_btn.visible or ov._continue_btn.visible:
|
||||
print("[reward_verify] FAIL — 选择态应三卡呈现且确认/继续隐藏(btns=%d)" % btns)
|
||||
quit(1)
|
||||
return
|
||||
for c in ov._choice_box.get_children():
|
||||
# ② 点选→高亮→确认外发(真实按钮路径);确认前改选以最后点选为准
|
||||
ov.press_card(0)
|
||||
ov.press_card(1)
|
||||
ov.press_confirm()
|
||||
if ov._confirm_btn.visible != true or picked.size() != 1 or String(picked[0]) != "gp":
|
||||
print("[reward_verify] FAIL — 点选确认未外发 pick_confirmed(gp)(%s)" % str(picked))
|
||||
quit(1)
|
||||
return
|
||||
# ③ 替换子面板:丢旧收新与放弃新件两路外发(通关渠道=携带序号 / 节点渠道=库存句柄——tag 直通)
|
||||
ov.open_replace([{"index": 0, "item_id": "probe_dust", "display_name": "显影尘"}], "爆破工兵包", 1, 1)
|
||||
var replace_btns := 0
|
||||
for c in ov._replace_box.get_children():
|
||||
if c is Button:
|
||||
replace_btns += 1
|
||||
if replace_btns != 2 or ov._choice_box.visible:
|
||||
print("[reward_verify] FAIL — 替换面板应 2 钮(N+1 选 N+放弃)且三卡隐藏(btns=%d)" % replace_btns)
|
||||
quit(1)
|
||||
return
|
||||
for c in ov._replace_box.get_children():
|
||||
if c is Button:
|
||||
c.pressed.emit()
|
||||
break
|
||||
if called.size() != 1 or String(called[0]) != "sapper_order":
|
||||
print("[reward_verify] FAIL — 选择未外发 item_chosen(%s)" % str(called))
|
||||
if replaced.size() != 1 or int(replaced[0]) != 0:
|
||||
print("[reward_verify] FAIL — 丢旧收新未外发 replace_chosen(0)(%s)" % str(replaced))
|
||||
quit(1)
|
||||
return
|
||||
print("[reward_verify] PASS — 池空直通/全列呈现/强制选择/选择外发 全过")
|
||||
# ④ 结果态:继续钮点亮、唯一出口
|
||||
ov.show_result("已选择:攻击力提升 · 攻击 +1")
|
||||
if ov._continue_btn.visible != true or ov._replace_box.visible:
|
||||
print("[reward_verify] FAIL — 结果态应点亮继续且收替换面板")
|
||||
quit(1)
|
||||
return
|
||||
ov.press_continue()
|
||||
if continued.size() != 1:
|
||||
print("[reward_verify] FAIL — 继续未外发 continue_pressed")
|
||||
quit(1)
|
||||
return
|
||||
ov.close_pick()
|
||||
if ov.visible:
|
||||
print("[reward_verify] FAIL — close_pick 应收屏")
|
||||
quit(1)
|
||||
return
|
||||
print("[reward_verify] PASS — 三卡两段式/确认外发/替换两路/结果态/收屏 全过")
|
||||
quit(0)
|
||||
Reference in new issue
Block a user