merge: 吸收冒险结构实现批(master 4f7f25d)——smoke 注册表与段体双留/06 双删/roadmap 同日行合并

This commit is contained in:
DKChan committed 2026-09-21 04:50:56 +08:00
commit ed75e0f10c
19 files changed
+852 -58

No files matched your search

+6 -5
View File
@@ -6,13 +6,13 @@
## 关卡
**三关已落地**,共用一条分支节点链模板(第二层幽暗地域随分支节点图批落地,第三层灰矮人矿城随第三关内容批落地;正式序列推进的收尾待办见[待办清单](06-待办清单.md)):
**三关已落地**,共用一条分支节点链模板(第二层幽暗地域随分支节点图批落地,第三层灰矮人矿城随第三关内容批落地;三关正式序列已随冒险结构实现批生效——首页「开始冒险」连探 L1→L2→L3):
| 层 | 层名 | 结构 | 状态 |
|---|---|---|---|
| 第一层 | 地精洞窟 | 分支节点图(战斗 / 随机道具 / 恢复点 / Boss 等节点型) | 已落地 |
| 第二层 | 幽暗地域 | 同一分支节点图模板复用,第二关深度轴行值填充 | 已落地(正式序列推进待收尾) |
| 第三层 | 灰矮人矿城 | 同一分支节点图模板复用,第三关深度轴行值填充 | 已落地(测试直入口;正式序列推进待收尾) |
| 第一层 | 地精洞窟 | 分支节点图(战斗 / 随机道具 / 恢复点 / Boss 等节点型) | 已落地(正式序列首关) |
| 第二层 | 幽暗地域 | 同一分支节点图模板复用,第二关深度轴行值填充 | 已落地(正式序列已生效) |
| 第三层 | 灰矮人矿城 | 同一分支节点图模板复用,第三关深度轴行值填充 | 已落地(正式序列已生效;测试直入口保留) |
现行节点行集与行值读 `data/tables/level.tres`(registry 为准)。再扩关卡走扩展批流程:先填深度轴行值、再接正式序列推进,不新造节点链结构。
@@ -56,7 +56,7 @@ MVP 不加新状态;扩容走 status_effects.tres 加行 + registry 登记。
垂直切片阶段明确砍掉或推迟的内容,写在这里防止回潮:
- 更多关卡(N>3)——第三关已按扩展批流程落地(行集 + 测试直入口);正式序列推进仍待 Alpha
- 更多关卡(N>3)——第三关已按扩展批流程落地(行集 + 测试直入口);三关正式序列已生效(冒险结构实现批 2026-09-21)
- 精英节点与词缀系统——只预留了字段位,不定义语义
- 地形变异扩展内容——首批四类型已落地,反制道具与全局作用域排 Alpha(2026-09-20 用户拍板)
- 道具/增益之间的组合协同(遗物式联动)
@@ -78,3 +78,4 @@ MVP 不加新状态;扩容走 status_effects.tres 加行 + registry 登记。
| v0.5 | 2026-09-20 | 层名二改批:「裂颚洞窟」→「地精洞窟」(用户拍板——不绑定 DND 专名部族、规避 IP 指涉,直取地精巢穴意象;v0.4 出典随之作废仅留档)。全仓 8 处消费面随批同步 |
| v0.6 | 2026-09-20 | **第三关内容批(灰矮人矿城——与切片代码 / registry / 难度曲线与Boss / 棋盘生成 / 地形变异 同批)**:层名表加第三层(测试直入口已落地、正式序列推进待收尾——正式三关序列推进仍登记 Alpha 验收条件);棋盘尺寸条款由两档改按关配置(第三关 13×13 / Boss 14×14 随批铺行);棋盘节地形变异句随批补第三关一对(落石/矿尘);暂不做「更多关卡」行更新为已按扩展批流程落地。行值读 `data/tables/level.tres` / `board_gen.tres` / `variation.tres`(registry 为准) |
| v0.7 | 2026-09-20 | **GP+商店房批(与切片代码 / registry / 关卡节点地图 / 冒险结构 / 战斗结算 / 地图战斗资源 / 难度曲线与Boss 同批)**:格面资源池"八型 × 正负两向(十六格)"→"九型 × 正负两向(十八格)"(+gp 金币);新增「商店」小节一行——shop.tres 读表纪律(商品三行 / 掉落参数 / 价格参数,全部 provisional 归曲线调参批);一句话定位句补"一套商店配置" |
| v0.8 | 2026-09-21 | **冒险结构实现批(与切片代码 / registry / 冒险结构 同批)**:层名表 L2/L3 状态行改「正式序列已生效」——首页「开始冒险」三关连探落地(AdventureRunner 编排;测试直入口保留);「更多关卡」行同步;正文零历史叙述纪律下仅状态口径更新,结构框架零改动 |
-26
View File
@@ -205,25 +205,6 @@
**状态**:已拍板登记(2026-09-18),待 Alpha 内容批执行
#### 切片行进代码与定稿的接口漂移
**现状**:切片行进器(LevelRunner)为单关 provisional 实现,与关卡节点地图 v1.0 定稿存在接口漂移,逐条登记:① `start_level` 载荷含种子并自开随机流(`rng.new_adventure`)——定稿:种子注入与随机流域归冒险结构,开始关卡载荷 = 关卡序号 + 当前生命 + 生效上限;② 主角生命在行进器本地持有——定稿:生命账本归冒险结构;③ 恢复兑付为行进器自扣(`_apply_rest`)——定稿:发治疗封顶回报、由冒险结构执行回血封顶;④ 战斗胜利自动推进(`_enter_next_node` 直调)——定稿:全节点确认 + 兑付后确认、零自动推进;⑤ `continue_after_node` 仅对奖励/恢复生效——定稿:泛化为全节点行进确认与选路动作;⑥ 收束点未编排"先调战斗结算关卡收束清库"步——定稿:先清库后宣布为强制次序;⑦ 战斗败北直接终关卡——定稿:每战结果上报冒险结构,倒下的失败结算归冒险结构;⑧ 节点图结构(节点 + 连线)与图合法性校验未实现——单链数组为特例可先行,图版随内容批。
**归属**:切片图版实现批(随第二关内容批 / 关卡节点地图 P1「分支节点图内容与实现批」)
**状态**:部分清偿(2026-09-17 分支节点图批)——④⑤⑧ 三条已清偿:战斗胜利改进选路态(`choices_presented`,零自动推进);`continue_after_node` 泛化为兑付确认(确认 ≠ 推进,选路动作归 `pick_next_node`);节点图结构 + 图合法性校验已落地(节点带 `next` 出边表,编号 = `node_depth`,编号唯一 / 入口唯一 / Boss 恒唯一终点 / 前进方向无环 / 双向可达六检,违规加载致命,L1 单链经同一套校验)。①②③⑥⑦ 属正式侧冒险结构接口,保持登记。
#### 两关序列未生效
**现状**:第二关深度轴行值填充批落地前,两关序列未生效,冒险以单关运行(合法态;扩展批已排 P1)。生效前提(第二关行值/图版填充)已于 2026-09-17 分支节点图批满足。注记(2026-09-17 分支节点图批):切片首页已加"关卡 2 试玩"直入口(测试模式 `start_level(2)`,与序列推进无关)——行值/图版前提已满足后,正式序列推进(关卡 1 通关接续关卡 2)仍未实现,本条保持登记。
**归属**:难度曲线与Boss(随第二关内容批);Alpha 验收条件登记
**状态**:待办(2026-09-19 用户拍板:垂直切片期可接受现状——测试直入口进关 2;**列入 Alpha 版本验收条件**,正式序列推进与切片行进器接口漂移 ①②③⑥⑦ 的对齐随 Alpha / 正式侧冒险结构批执行)
#### 大奖励数值随难度曲线联动
**现状**:上限提升与恢复比例同通关率带、换算率锚的联动复验,随难度曲线与Boss的调参批执行
@@ -330,13 +311,6 @@
### 数据配置
#### 冒险表未建表(原「关卡表与冒险表未入表清单」关卡表半边已收口)
**现状**:关卡表半边已收口(2026-09-18 清理批核销:level.tres + LevelTable schema + config_db 加载行齐备,切片加载清单在册);冒险表 `AdventureTable` 仍未建——registry 已登记字段(keystone_per_level,单行全局,Rule 4 豁免)且注记「未建表零代码影响」,切片无消费方(元进度未实现),建账归冒险结构定稿批
**归属**:冒险结构定稿批(随批建账)
**状态**:待办(仅剩冒险表半边)
### 教学引导
@@ -284,3 +284,4 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗
| v0.3 | 2026-09-17 | 代币→钥石统一拍板(用户裁决):大奖励第三件「代币记账 +10」改「钥石入账 +10」——与元进度钥石同一资源(渠道 = 关卡大奖励入账与 Boss 掉落),「代币」措辞退役;调参表字段名、验收标准、依赖表同步。数值与机制不变 |
| v0.4 | 2026-09-17 | 数值纪律收口批:正文与调参表 knob 现行值改指路存根(事实源 data/tables/*.tres 与 registry),3 处失同步修正(示例轨迹改纯符号〔旧 50/50 生命轴示例作废〕、大奖励 +10/+10 改读 adventure_table 行值并删换算率数字锚、调参表 30/10/10 三格改读 level.tres / adventure_table) |
| v0.5 | 2026-09-20 | **GP+商店房批(用户拍板——与关卡节点地图 v1.1 / 战斗结算 v0.23 / 地图战斗资源 v0.15 / 难度曲线与Boss v0.18 / registry / 切片同批)**:① 新增「GP 冒险账本」节——钱包三收口(战斗完成统一入账 / 商店购买出账 / 冒险收束清零)+ 战斗内缓冲隔离语义(格面兑付入缓冲、战斗完成统一到账、战斗域零钱包状态);② 统一冒险加算区成文——商店购买与关卡大奖励上限提升同一本账(三轴 {攻击, 生命上限, 护甲等级},经覆盖值通道生效;购血 = 上限与当前生命同步 +Δ);③ GP 掉落掷定消费 gp_drop 封闭随机流(主种子派生)登记进主种子节与调参表;④ GP 不入存档、不入冒险摘要;范围表 / 大奖励表 / 依赖表 / 验收标准同步。数值全 provisional-pending-曲线调参批,行值读 shop.tres |
| v0.6 | 2026-09-21 | **冒险结构实现批落地登记(切片 AdventureRunner + LevelRunner 账本外置 + AdventureTable 建表——与切片代码 / registry / 03 / 06 / roadmap 同批)**:本批只记实现批状态,正文规则口径零改动。落地面 = 三关正式序列生效(首页「开始冒险」)、接口契约四件套正式侧承载(开始关卡 start_level_external / 战斗结果上报共享账本直写 / 治疗封顶回报 apply_rest / 关卡结束 level_finished)、生命账本与 GP 冒险账本上交冒险结构层、大奖励非末关恰兑付一次(末关钥石照记)、冒险摘要五件事产出、主种子恰一次注入(registry rng_service new_adventure 调用权同批登记)、结算受理只算一次;06 待办「两关序列未生效」「切片行进代码与定稿的接口漂移」「冒险表未建表」三条随批解决移除。**规划书冲突按代码为准登记两处**:①大奖励 `_pending_next_level` 置位先于 emit(信号同步派发——处理器内立即确认需标记已就位);②放弃受理补 state=running 守卫(契约 7 结算受理只算一次的 abandoned 侧对齐) |
+4 -3
View File
@@ -23,6 +23,7 @@ entities:
streams: [board_gen, reward_roll, encounter, armor_roll, behavior_roll, variant_place, gp_drop]
stream_names: 当前封闭清单(增流需改代码 + P1 支柱评审;combat_var 已于 2026-09-07 full 评审删除——玩法随机不预置基础设施;encounter 增流 2026-09-12 用户拍板——遭遇随机化批,scope_event_binding = 每次战斗实例化恰一 scope;**armor_roll 增流 2026-09-16 用户拍板**——护甲等级掷点批,回合内随机首次入流:scope_event_binding = 每次掷点恰一 scope〔逐次掷逐次取号、可逐位回放〕,消费面 = 战斗结算 吃掷点分量判定;**behavior_roll 增流 2026-09-16 用户拍板**——敌人技能池批(开轮摇号):回合内随机、时点前置到每轮开轮〔结算步骤流之外〕,scope_event_binding = 每敌每摇号恰一 scope,**派生向量 = spawn_index × 1000000 + 单敌摇号序**〔单敌出招轨迹可独立复放、敌互不穿插;取号门 = open_scope(stream, scope) 显式 scope 取号——计数器仍单调 +1 供观测〕,消费面 = 战斗结算 开轮摇号换算层 + 技能摇号器 SkillLottery;**variant_place 增流 2026-09-20 用户拍板**——地形变异首批类型定稿批:scope_event_binding = 每次装板恰一 scope〔常规换图/轮内刷新图各取号一次〕,消费面 = 战斗结算 布置段 _place_variants——变异类型选取 + 连通片生长取数;**gp_drop 增流 2026-09-20 用户拍板**——GP+商店房批:scope_event_binding = 每场战斗结束的掉落掷定恰一 scope,消费面 = 战斗结算 战斗结束分支——按战斗节点身份查商店表掷定 GP 掉落,掷定 + 关级加值 + 战斗内缓冲合成本战 gp_dropped〔shop_roll 流随"无三选一摇号"裁决作废、从未入册〕)
stream_facade: open_stream(stream) 取号(计数器+1→派生→返回已播种实例);derive_at(stream, scope) 纯函数供测试/预览(不推计数器)
new_adventure: 冒险开始重播种入口——调用权 = AdventureRunner.start_adventure 唯一正式调用方(切片单关测试路径 LevelRunner.start_level 除外;冒险结构实现批 2026-09-21——主种子全冒险恰一次注入,冒险内不重置不逐关播种)
scope_event_binding: board_gen=每次生成新扫雷图(棋盘生成);reward_roll=每次 board install(格奖励批 + 清图奖励批)(地图战斗资源——2026-09-09 接口漂移改写,原"每次奖励结算 roll 批次";地图战斗资源 Rule 2/AC-1 为契约源);encounter=每次战斗实例化恰一 scope(战斗结算 begin_battle 实例化期、扫雷开始前——unit_count 区间均匀骰,挖掘决策域之外 P1 合规);armor_roll=每次掷点恰一 scope(战斗结算 战斗段吃掷点分量逐次判定——1d20,护甲等级掷点批 2026-09-16);variant_place=每次装板恰一 scope(战斗结算 布置段——地形变异首批类型定稿批 2026-09-20);gp_drop=每场战斗结束的掉落掷定恰一 scope(战斗结算 战斗结束分支——GP+商店房批 2026-09-20)
unseeded_access: 致命中止
snapshot_contract: 续玩/复盘快照 = master_seed + 各流 scope 计数器(消费方 存档序列化)
@@ -554,17 +555,17 @@ entities:
nodes: 节点图结构——节点数组 + 连线(node_kind ∈ {battle, reward, rest, boss, shop},切片字段名 type;+shop 随 2026-09-20 GP+商店房批——商店房兑付 = 打开商店覆盖层、离开后确认回选路,不携带战斗行〔零敌人、零棋盘配置行〕;每节点领关卡内唯一**节点编号**,battle/boss 数据键 node_depth 沿用〔v1.0 语义升级:深度 → 节点编号〕;连线 = 出边节点编号表,只朝前进方向、无回头路);单链特例 = 隐式后继(数组序即链,切片现行形态零漂移)
rowset_rule: 图合法性校验(2026-09-17 关卡节点地图 v1.0)——节点编号关卡内唯一、入口唯一、Boss 恒唯一终点(编号最大,Dmax 语义平移)、连线无环、所有节点沿连线可达 Boss,违规加载期致命;实现批(2026-09-17 分支节点图批):校验落 LevelTable.validate(单链形态经同一套算法——编号解析单链 = 数组下标 / 图 = node_depth),L1 单链与 L2 图双行集加载全绿断言;商店房编排(每关恰 1、Boss 前一位必经位)由冒烟断言承载、非加载校验——图合法性六检零新增(GP+商店房批 2026-09-20,关卡节点地图 v1.1)
cross_table_validation: battle/boss 节点 (L,D) 在 board_gen/enemy 两表存在精确行(数据配置 跨表校验⑧复用 + 冒险结构 AC-7 冒烟断言——序列关数 ⊆ 内容覆盖,越界关卡 = 内容生产缺口)
mvp_content: L1 全行(切片转正——7 节点链〔单链特例,合法图形态〕、rest_heal_pct=30);L2 全行已落地(2026-09-17 分支节点图批——首张分支图:9 节点〔入口 0 → 中间三层 2/3/2 → Boss 8〕、真实岔路 4 处、奖励点+恢复点各 1、战斗节点 6 + Boss;行值 provisional 归 难度曲线与Boss v0.7;首页直入口试玩 ≠ 两关序列生效,正式序列推进仍登记待办);**L3 全行已落地(2026-09-20 第三关内容批——「第三层 · 灰矮人矿城」:九节点图复用 L2 同模板〔6 战 + Boss + 奖励 + 恢复、真实岔路 4 处〕,行值全 provisional 归 难度曲线与Boss;测试直入口 ≠ 序列生效——正式三关序列推进仍登记 Alpha 验收条件);2026-09-20 GP+商店房批 rewiring:L2/L3 图版 Boss 重编号 8→9、各新增 (8, shop, next[9]) 商店房节点(Boss 前一位必经位),L1 零改动,图合法性六检零新增——必经位由冒烟断言承载**
mvp_content: L1 全行(切片转正——7 节点链〔单链特例,合法图形态〕、rest_heal_pct=30);L2 全行已落地(2026-09-17 分支节点图批——首张分支图:9 节点〔入口 0 → 中间三层 2/3/2 → Boss 8〕、真实岔路 4 处、奖励点+恢复点各 1、战斗节点 6 + Boss;行值 provisional 归 难度曲线与Boss v0.7;首页直入口试玩模式保留);**L3 全行已落地(2026-09-20 第三关内容批——「第三层 · 灰矮人矿城」:九节点图复用 L2 同模板〔6 战 + Boss + 奖励 + 恢复、真实岔路 4 处〕,行值全 provisional 归 难度曲线与Boss;测试直入口保留)**;2026-09-20 GP+商店房批 rewiring:L2/L3 图版 Boss 重编号 8→9、各新增 (8, shop, next[9]) 商店房节点(Boss 前一位必经位),L1 零改动,图合法性六检零新增——必经位由冒烟断言承载;**2026-09-21 冒险结构实现批:三关正式序列生效**(首页「开始冒险」= L1→L2→L3 连探,AdventureRunner 编排——大奖励/生命账本/GP 冒险账本/主种子归冒险结构层,冒烟断言在案)**
referenced_by: [冒险结构, 关卡节点地图, 难度曲线与Boss, 棋盘生成, 数据配置]
- name: adventure_table
type: schema_data
owner: design/gdd/systems/冒险循环/冒险结构.md
attributes:
table: AdventureTable(数据配置 承载,单行全局;冒险结构 回填 owner;Rule 4 豁免——单行全局常量表,HeroTable/SettlementTable 先例;建表走 数据配置 接口漂移规则)
table: AdventureTable(数据配置 承载,单行全局;冒险结构 回填 owner;Rule 4 豁免——单行全局常量表,HeroTable/SettlementTable 先例;建表走 数据配置 接口漂移规则。**已建表**——data/schemas/adventure_table.gd + data/tables/adventure.tres(两字段 10/10)+ ConfigDB 加载行"adventure",冒烟断言在案(冒险结构实现批 2026-09-21))
fields:
level_clear_hp_bonus: int [0,999](knob 默认 10——关卡大奖励 HP 上限提升,冒险内生效经 begin_battle 覆盖值通道传入后续每战;值锚 = 难度曲线与Boss 区间平移换算率 +10hp ≈ +1 轮,上调须 难度曲线与Boss 重扫 L2 通关率带)
keystone_per_level: int [0,999](knob 默认 10——每通关一关的钥石入账值;消费方 元进度;2026-09-17 代币→钥石统一拍板改键〔原 token_per_level〕——AdventureTable 未建表,零代码影响)
keystone_per_level: int [0,999](knob 默认 10——每通关一关的钥石入账值;消费方 元进度;2026-09-17 代币→钥石统一拍板改键〔原 token_per_level〕;AdventureTable 已建表——冒险结构实现批 2026-09-21,零代码影响注记随之失效)
mvp_content: 两字段默认值即 MVP 行(10 / 10,provisional)
extension_reserve: 大奖励形态扩列位(三选一 / 攻击型 / 通关加成——冒险结构 OQ2,随元进度设计)
referenced_by: [冒险结构, 元进度, 数据配置]
+4 -3
View File
@@ -16,15 +16,16 @@
| 2026-09-15 | 命名清偿批;格面资源批(五段结算/极性路由/可选类删除);攻击类型与奖励抽象分离;护甲等级登记批;伤害量值区间批 |
| 2026-09-16 | 行动点压速批 8→6;护甲等级掷点批落地(smoke 233 全绿);AP6+AC 验收判读口径 v1.0;06-待办清单建账改版;系统更名批;敌人技能池提案对答启动 |
| 2026-09-17 | 敌人技能池提案定稿(1c63830);技能池实施批落地(smoke 263 全绿,待收编);魔法道具切片期空内容拍板;关卡节点地图定稿批解锁;跨会话状态持久化批(快照节 + 本文件 + 守卫文案升级) |
| 2026-09-21 | 存档序列化批(SaveManager 落地:单档 JSON 存档读写/版本迁移/损坏兜底 + 元进度四件与冒险摘要入档,smoke 463 全绿);GP+商店房实施批;第三关内容批;八型批等 2026-09-20 多批(详见 active.md 台账 96-116) |
| 2026-09-20 | 第三关内容批(灰矮人矿城行集 + 测试直入口,smoke 375 全绿);GP+商店房实施批(九型格面+随机掉落+Boss前商店房+统一加算区,smoke 437 全绿);八型批呈现层漏网修复(真人试玩卡断——行协议呈现层同步闸 11 断言) |
| 2026-09-21 | **冒险结构实现批:切片从三个单关 demo 升级为一局完整三关冒险**(AdventureRunner+LevelRunner 账本外置+AdventureTable 建表;三关正式序列生效;smoke 482 全绿);存档序列化批(SaveManager 落地:单档 JSON 存档读写/版本迁移/损坏兜底 + 元进度四件与冒险摘要入档)——元进度批(钥石消费)挂点就位 |
## 路线图(主线路线与门)
1. **技能池实施批(已落地,smoke 263 全绿;待一笔收编提交)**:批 1 核心机制 + 批 2 呈现替代 + 批 3 文档与 registry 已完成;收编提交后本站闭合。设计权威 = `docs/proposal-skill-pool.md`。
2. **验收批**(门 = 技能池批收编 + 工作区净 + smoke 全绿):按 `docs/acceptance-ap6-ac.md` v1.0 跑三数据源(蒙特卡洛双档 / 真人 3–5 局 / 日志分析器),无道具基线;产出判读结论与选档建议。
3. **难度曲线阶段二调参批**(门 = 验收读数):四级阶梯落档 + 三杠杆扫描(血量锚走廊 / 群战上探 / 注入衰减)+ 挖掘风险不等式与清图支配性两对账重跑 + AC 行值投值。
4. **第二关内容批**(门 = 调参行值):深度轴行值 D1–D4 填充 + 新内容投放(技能池定稿后 = 既有池新技能);填齐后两关序列生效。
5. **并行 / 后置轨道**:关卡节点地图 GDD 定稿批(已解锁,与在飞批零文件冲突);魔法道具内容批(排切片收尾后);真人通关率标定(挂 playtest 批);元进度定稿(排 Alpha 前收口——资源关系已裁统一钥石〔2026-09-17〕,剩解锁清单/定价与钥石产出细则)。
4. **深度轴行值填充批(原第二关内容批的行值半边;门 = 调参行值)**:L2/L3 行集已落地、三关正式序列已生效(2026-09-21 冒险结构实现批);剩深度轴行值 D1–D4 投值与新内容投放,归难度曲线阶段二调参批同窗。
5. **并行 / 后置轨道**:存档序列化批(挂点 = 冒险状态与摘要——冒险结构实现批已就位);元进度批(钥石消费方挂点 = 冒险摘要钥石合计——已就位);魔法道具内容批(排切片收尾后);真人通关率标定(挂 playtest 批,指引见切片 README)。
## 维护纪律(收口批五步)
+11
View File
@@ -12,8 +12,12 @@
## 当前状态快照(收口批先更新本节,再追加台账;≤10 行)
- **更新于**:2026-09-21(**冒险结构实现批——三关正式序列生效**,worktree adventure-structure-20260921,台账 117):按用户规划书(.zcode/plans/adventure-structure-batch-plan-2026-09-21.md)T0–T9 执行——切片从三个单关 demo 升级为一局完整三关冒险:AdventureTable 建表(adventure.tres 10/10)+AdventureRunner 新建(主种子恰一次/生命账本/GP 冒险账本/统一加算区/大奖励非末关恰兑付/三结局摘要五件事)+LevelRunner 账本外置(hero_hp/gp 属性化共享账本,接口漂移①②③⑥⑦清偿)+首页「开始冒险」三关连探(大奖励屏+冒险结算屏);06 三条目解决移除(两关序列未生效/接口漂移/冒险表未建表)。代拍板七项按规划书 §7 采行待确认(详见台账),规划书冲突按代码为准三处登记。冒烟 437→482 项 0 失败三闸全过+四探针 PASS;MC 回归逐位一致(旧单关路径零行为变更证明);文档三工具与基线一致
- **更新于**:2026-09-20(**GP+商店房实施批(L3-A 玩家成长批)**,worktree gp-shop-20260920,台账 116):用户六问裁决落地——①GP 进格面第九型(金币袋 weight 2 power 3 低概率惊喜+破财字典负向行,正向合计 76);②掉落=均值±方差掷定(普通 3[2,4]/Boss 8[6,10]/精英 5[4,6] 预留)+关级加值(L2 +1/L3 +2),gp_drop 新封闭流(黄金向量已登);③记账=战斗完成统一到账,格面兑付走战斗内缓冲 escrow 与钱包隔离;④商店房 Boss 前一位必经位(哈迪斯式,L2/L3 行集 Boss 重编号 8→9);⑤统一冒险加算区(商店购买与大奖励同账,购血上限当前同步);⑥三商品全陈列无刷新无三选一,价格=基价+关级加成+步长×已购档;数值/分层验收整体后置归曲线调参批(骨架先行)。冒烟 437 项 0 失败+三探针 PASS;GDD 八文档(关卡节点地图 v1.1 五类枚举/冒险结构 v0.5 GP 账本/战斗结算 v0.23/地图战斗资源 v0.15 九型/曲线 v0.18 杠杆登记/元进度 v1.2/03 v0.7/05 红线例外成文)+06 GP 条目收口+「关卡结构随机生成」方向观察登记
- **更新于**:2026-09-20(**第三关内容批 + GP 定稿提案批**,worktree l3-content-20260920,台账 115〔113/114 号让给并行在飞会话两批,顺延让号〕):L3-B 落地——第三层「灰矮人矿城」行集(enemy 七行 duergar/hook_horror/duergar_king + board 13×13/Boss 14×14 词表值 deep_underdark + 九节点图 + 落石/矿尘 + 大地脉动=sapped 首发)+ 关卡 3 测试直入口 + 冒烟 375 项全绿 + registry 五条目 + GDD 五文档(03 v0.6/曲线 v0.17/棋盘生成 v0.4/地形变异 v0.4/06 GP 条目立项提前);板形验算 13/14×14 推理率 0.94–0.96 全达标零降档;MC 读数:零成长基线普通 16%/完美 20%(N=300,死亡 75–80% 集 Boss——预案「必红」预判被棋盘武器库部分证伪,成长批必要性不变)+ 成长代理档 40%/80%;**GP+商店房定稿提案已交付**(proposal-gp-shop-room-2026-09-20.md,10 拍板点:格面池零修订/商店房必经位/三属性 flat 档 攻+1=4 血+3=2 甲+1=4),待用户逐条拍板后进波次 2 实施批
- **更新于**:2026-09-20(**ComfyUI 出图提示词与规格卡批**,worktree art-prompts-20260920,台账 114):`design/art/comfyui-prompts.md` v1.0 成文——美术清单 ComfyUI 通道执行侧:主角三态+六敌种逐资产提示词卡(风格条款转写 art-bible:亮描边轮廓通道进正负双侧、紫赤 300–345° 禁入带写入全部卡片);兄弟精改荐脚本换色通道;候选入库→裁决→后处理→转正五步流程+验收速查;两份清单各补指路行;纯文档零代码,doc 门禁与基线一致(合并 31d2710)
- **更新于**:2026-09-20(**八型批呈现层漏网修复批〔真人试玩卡断〕**,worktree battlelog-attachfix-20260920,台账 113):八型批伏击退役拆除伤害行 attach 字段后战斗日志视图拆行分支漏网——敌方首笔攻击行格式化即 Invalid access、结算段中途夭折=界面卡断;修复=基线+伏击与三分量两退役拆行分支拆除+smoke 新增行格式化呈现层同步闸 11 断言(行协议 9 类自此有呈现层回归网);冒烟 355→366 项 0 失败三闸全过+四探针全过;纯呈现层零行值零机制改动(合并 931bba7)
- **更新于**:2026-09-20(**地穴全图卷并入 Alpha 魔法道具整体重设计批**,worktree magic-fold-20260920,台账 112):用户拍板「垂直切片阶段不保留魔法道具相关内容」——06「地穴全图卷效果重定」删除并入 Alpha 魔法道具整体重设计(口径同台账 98 显影尘批,清单 44→42 条);顺带清除「零伤害/无威胁组合的跨字段护栏」死条目(台账 100 已裁决删除,八型批合并 3468066 冲突解决误取分支侧复活,本批回归修复);悬空指针三处清偿(魔法道具.md 存根行/03-内容规模 待定句/registry catacomb_scroll 行内措辞);纯文档零代码冒烟不适用,doc 门禁与 master 基线一致
- **更新于**:2026-09-20(**第一层定名「地精洞窟」二改批**,worktree rename-l1-goblin-20260920,台账 111):用户拍板「不用这么 DND,就叫地精洞窟」——终名不绑定 DND 专名部族、规避 IP 指涉;「裂颚」出典作废仅留档,8 处消费面与提案层名行/沿革注记随批同步(03-内容规模 补 v0.5);纯字面变更零行值零机制改动,冒烟 355 项 0 失败
- **更新于**:2026-09-20(**第一层改名「裂颚洞窟」批**,独立 worktree 分支 rename-l1-20260920,台账 110〔登记时撞并行会话 109 号,顺延让号〕):用户拍板「苔藓回廊与地精不契合」作为过期词语全仓退役——新名致敬 DND 5e《凡戴尔的失落矿坑》地精部族「裂颚」(Cragmaw)洞窟巢穴,洞窟纵深连通幽暗地域、与第三关预案「灰矮人矿城」构成三层纵深叙事线;八处面同步(level.tres display_name+表尾批注 / ui_assembler 首页按钮 / 切片 README×2 / 00-游戏概念 / 03-内容规模 v0.4 / registry level_table / 第三关提案层名行+蕈林理由重写+两处旧读数顺手校正);纯字面变更零行值零机制改动,冒烟 355 项 0 失败
- **更新于**:2026-09-20(**地图战斗资源正文历史叙述迁出批**,worktree doc-polish-20260920,台账 109):变更记录归版本历史的纪律成文(正文零历史叙述),极性矩阵节两处旧体系对照句迁出;doc_term_audit 疑似 3→2
@@ -288,5 +292,12 @@
109. **地图战斗资源正文历史叙述迁出批(2026-09-20,本会话——用户对 doc_term_audit 剩余 3 疑似逐条裁决:①battle ambusher 留档表可接受〔废案存档不影响使用〕、②极性矩阵正文的历史对照句应迁出——变更记录应如 ADR 记于版本历史而非系统描述正文、③06 雾格失义随待办完成自然消解;worktree doc-polish-20260920,合并 3abc2a1,纯文档 1 文件 2 行)**:①极性矩阵节「取代旧体系的四套固定兑付」历史对照句删除(信息已在版本历史 v0.4 全量留档);②节尾「退化特例/撤旗退役变更叙述」删除,改一句纪律成文:**矩阵只描述现行规则,被取代的旧体系与历次结构变更一律记版本历史,不在正文复述**;③验证=gdd_lint 与 master 基线 diff 只减不增(-1 条 R3)、doc_term_audit 疑似现行 3→2(余 2 处=用户判定留档:battle ambusher 留档表、06 雾格失义陈述)。**纪律首例**——本批确立「正文零历史叙述」文档纪律,后续各系统文档的历史对照句按同口径迁移(未排期,遇批顺带)
110. **第一层改名「裂颚洞窟」批(2026-09-20,本会话——用户拍板「苔藓回廊跟地精不太契合……直接把苔藓回廊作为过期词语删除替换掉」,新名授权本会话选定;worktree rename-l1-20260920)**:全仓「苔藓回廊」现行用字清零(8 处消费面全替换,仅 5 处退役审计注记有意保留——版本历史/registry 注记/表尾批注/提案沿革,符合文档纪律)——①切片 `level.tres` display_name →「第一层 · 裂颚洞窟」+表尾改名批注;②切片首页按钮 `ui_assembler.gd`;③切片 README 试玩指引×2;④`00-游戏概念` 首关层名行;⑤`03-内容规模` 层名表+版本历史 v0.4;⑥registry `level_table` display_name 行。**命名出典**:新名致敬 DND 5e《凡戴尔的失落矿坑》(Lost Mine of Phandelver)地精部族「裂颚」(Cragmaw)洞窟巢穴——地精盘踞的浅层洞窟、通道向下延伸天然连通幽暗地域,且 LMOP 反主即卓尔「黑蜘蛛」,与 L2 蛛化卓尔 Boss 同谱;三层纵深叙事线贯通:裂颚洞窟(地精部族)→ 幽暗地域(蛛群·卓尔裔)→ 灰矮人矿城(第三关预案方向,未拍板)。第三关提案随批同步:层名行 + 蕈林备选理由重写(原理由绑「苔藓」已失义)+ 层名沿革注记 + 两处旧读数顺手校正(L2 完美 Boss 承伤改 51.6% 重投基线口径〔原 55.8% 系台账 96 地形批读数、已被台账 105 重投翻页〕;通关率带改分带后 L1 [35,55]/L2 [25,45])。仅字面变更:display_name 无代码/探针消费方(grep 证实),节点结构/行值/机制零改动。**验证**:冒烟 355 项 0 失败(tools/run_smoke.py 三闸全过;全新 worktree 首跑撞 class_name 缓存坑,对切片目录 `--editor --quit` 刷缓存后过——十二坑清单第 1 条再验证);全仓 grep 苔藓仅剩审计注记。
111. **第一层定名「地精洞窟」二改批(2026-09-20,本会话——用户拍板「不用这么 DND,就叫地精洞窟就行了,不用明确说明是某个部族,毕竟我们没有 DND 的相应版权」;worktree rename-l1-goblin-20260920,纯字面零机制)**:台账 110 的「裂颚洞窟」(DND 5e《凡戴尔的失落矿坑》Cragmaw 致敬名)全仓退役改定「地精洞窟」——理由 = 终名不绑定 DND 专名部族、规避 IP 指涉。8 处消费面同步(level.tres display_name+表尾二改批注 / ui_assembler 首页按钮 / 切片 README×2 / 00-游戏概念 / 03-内容规模 层名表+版本历史 v0.5〔v0.4 出典作废留档〕 / registry level_table / 第三关提案层名行+沿革注记改写);「裂颚」字样仅存于版本历史/批注/沿革等审计记录(有意保留)。洞窟纵深连通幽暗地域的三层纵深叙事线取向不变(地精洞窟→幽暗地域→灰矮人矿城)。**验证**:冒烟 355 项 0 失败三闸全过(worktree 建起即对切片目录预刷类名缓存——十二坑清单第 9 条路径坑的规避姿势);grep 裂颚非审计命中 0。
112. **地穴全图卷并入 Alpha 魔法道具整体重设计批(2026-09-20,本会话——用户拍板「统一放在另一个'魔法道具重新设计'的待办项里面,垂直切片阶段不保留魔法道具相关的内容」;worktree magic-fold-20260920,合并 fbb1488,纯文档零代码)**:**裁决面**——06 清单「地穴全图卷效果重定」(原两方案待拍板:纯全图预读/另配信息半边)按用户指令删除,去向=并入 **Alpha 阶段魔法道具整体重设计**(解决即移除口径;清单 44→42 条)——至此垂直切片期魔法道具族待办全部出清(显影尘台账 98 先例、探宝罗盘 2026-09-19 已排 Alpha)。**顺带回归修复**——「零伤害/无威胁组合的跨字段护栏」死条目清除:该条已随台账 100 伤害下限批解决移除(6a9f590),八型批合并 3468066 冲突解决误取分支侧复活(父一 master 0 处 × 父二 ambush 分支 1 处 → 合并结果 1 处,git 考古实证),本批重落地已裁决删除态。**悬空指认清偿三处**:①魔法道具.md 五维度表信息维度存根行——地穴全图卷半边并入显影尘同句改指 Alpha 重设计;②03-内容规模.md 待定句同口径合并;③registry aux_items catacomb_scroll 行内「显影半边待重定」改「效果待重定·随魔法道具整体重设计归 Alpha」(表头 effect_kinds/mvp_content 两处 Alpha 指针台账 98 已覆盖,不动)。**边界**——00-游戏概念/元进度道具工坊的地穴全图卷罗列系登记现状陈述非待办指针,未动;探宝罗盘条目已排 Alpha 维持原状。**验证**——doc_link_audit --strict 与 gdd_lint 均与 master 基线一致(exit 1 系既有白名单欠账非本批引入);冒烟不适用。
113. **八型批呈现层漏网修复批(2026-09-20,本会话——用户真人试玩遇界面流程卡断并提交 Godot 错误堆栈 `BattleLogView._format_row: Invalid access to property or key 'attach'`;worktree battlelog-attachfix-20260920,合并 931bba7)**:**根因**——八型批伏击退役(台账 101)拆除伤害行 attach 字段后,战斗日志视图两处退役拆行分支漏网未同步(战斗结算 §行协议「行协议变更必须同步呈现层的行格式化」义务违约):①基线+伏击拆行分支见 baseline 在场即读 row.attach——敌方行动伤害行(编排器 payload 带 baseline 从不带 attach)每场首笔敌方还手即 Invalid access,错误发生在结算战斗段中途→段流程夭折=界面整体卡断;②普通+穿刺+魔法三分量拆行分支随 2026-09-19 充能退役批同样过期(现行单分量 payload 只带实际攻击类型一键),主角营地换型穿刺/魔法攻击时同型崩溃(潜伏雷,用户尚未踩到)。**修复**——两拆行分支拆除,行上归因读数只剩掷点档位随行(2026-09-16 掷点批口径不变);playtest_logger payload 字段注记 attach→rider 同步;纯呈现层零行值零机制改动,行协议文档无改动(文档侧八型批已同步、漏网仅在代码)。**测试基建**——smoke 新增「战斗日志视图行格式化呈现层同步闸」段 11 断言:现行形态敌方/主角伤害行逐型喂真实视图+退役文案不再出现+未生效聚合 ×2+其余六行型逐型解析文本增长(格式化中途夭折=零增长,脚本错误静默吞断言盲区转可见 FAIL)——行协议 9 类自此有呈现层回归网;此为「逻辑层冒烟抓不到 UI bug」的首个仓内对策,UI 驱动夹具(真场景+合成输入+截图+超时护栏)仍待立项。**验证**——冒烟 355→366 项 0 失败三闸全过;四场景探针全过(ui_probe/tune PASS、click 问题=0、layout 全量转储 exit 0)。**附答**——用户另问第一关"?"格=藤蔓缠绕(信息型地形变异数字遮蔽,台账 96 设计行为非 bug,已答)。
114. **ComfyUI 出图提示词与规格卡批(2026-09-20,本会话——用户请托「根据美术待生成清单补充对应的 prompt 还是具体的规格」,asset-spec 流程产出;worktree art-prompts-20260920,合并 31d2710,纯文档零代码)**:新建 `design/art/comfyui-prompts.md` v1.0——美术清单 ComfyUI 通道(角色级资产)的执行侧提示词卡:①主角三态(待机/攻击/受击)+六敌种(地精/熊地精普通精英两版/食人魔酋长/巨蜘蛛/兄弟精/蛛化卓尔精灵 Boss)逐资产正向提示词+通用正负底座,风格条款全转写 art-bible——§3.1 亮色描边+落影底盘轮廓通道(正向声明+负向禁黑描边)、§4.2 语义色禁区(紫赤 HSV 300°–345° 禁入带写入除 Boss 外全部卡片负向;Boss 卡反向主动取色+吸色验收专属注记);②通用规格表:1x 画布工作假设(主角 32×48 站姿样张档/普通怪 44×36 宽≥高/精英 44×44/Boss 64×72,标注一屏样张批冻结)、生成底色定纯浅灰 #F2F2F2(弃绿幕/品红幕理由=品红落禁入带、抠边留违规色相污染)、每资产 2–3 版候选+同 seed 同形象纪律;③兄弟精改荐**脚本换色通道**(换色不换形=原则三角色侧应用,ComfyUI 直出降备选,验收=与熊地精终稿剪影叠加逐像素一致);④五步流程=出图→候选入库 design/art/candidates/<资产>/→用户裁决→后处理(去底/最近邻降采样/色板量化/四联测试条,脚本管线代跑)→转正,附验收速查五关(灰度剪影认档/轮廓通道深浅底可辨/吸色扫禁入带/2–3 头身/同形象并排);⑤两份清单各补指路行(art-assets-checklist §2 头+asset-checklist 渠道表尾)。**边界**——动画类(意图三档/演出三类/连锁坍塌)明确不在本文件(一屏样张锁方案后另立规格卡);脚本管线资产(徽章 14 枚/节点图/砖面终稿)不需要提示词(规格锚 art-bible §4.6 图标键契约)。**验证**——doc_link_audit --strict 与 gdd_lint 均与 master 基线一致(31 文件/769 链接/死链 7 处全在 design/ux/battle.md 既有欠账、gdd_lint 8 文件既有,均非本批引入);冒烟不适用;worktree 与分支已清。
115. **第三关内容批 + GP 定稿提案批(2026-09-20,本会话——用户对 proposal-level3-content-2026-09-20.md 五拍板点全按推荐通过(灰矮人矿城/GP+商店房提前捆绑/乏力首发/板面+地形杠杆/测试直入口),plan 模式三子代理并行执行;worktree l3-content-20260920)**:**L3-B 第三关内容批**——①数据表:enemy.tres 七行(duergar 灰矮人重甲 15 血/灰矮人重装手 16/小队 8、hook_horror 钩爪恐魔轻甲 5–6 血群行〔复用 bite 共享行〕、Boss duergar_king 灰矮人石君王 75 血重甲=Σ91×r_boss≈0.82 锚;两敌种首遇位节点 0/1 固定数量不加骰;armor_class 恒 0 归在案投值条目);board_gen.tres 七行(战斗 13×13/Boss 14×14、d0.25、t0.88、词表第三值 deep_underdark);level.tres 第三关九节点图(与 L2 同模板);variation.tres 落石(flat_damage 3,1→2→3 递进)+矿尘(hide_number);skills.tres earth_pulse 大地脉动(妨害型=sapped 乏力第二词汇首发,字段口径同 web_snare);②代码:board_gen_table.gd 词表 v1.1+1、关卡 3 测试直入口(ui_assembler lv3btn+main 接线)、smoke_all.gd L3 断言段(图结构/跨表 7 坐标/定点/物种集八敌种/首遇绿档成长代理档口径 atk+200·hp+100 十二种子);③registry 五条目(enemy/board_gen_table/skill/variation_table/level_table);④GDD 五文档:03 v0.6(三关已落地/棋盘按关配置口径)/难度曲线与Boss v0.17(第三关图版注记+L3 provisional-pending-growth 口径)/棋盘生成 v0.4(词表 v1.1)/地形变异 v0.4(六类型两轴×三关)/06(GP 条目立项提前);**验证**——冒烟 375 项 0 失败三闸全过+三 UI 探针 PASS;板形验算 13×13 推理率 0.950–0.959/14×14 0.941 全达标零降档(降档梯子未触发);Boss max_gen_attempts 36→32(越 schema 域 [1,32],自决取域上限并批注登记);**MC 读数(unique)**——零成长基线 N=300:普通 16%/完美 20%,死亡 75–80% 集中 Boss(8.boss 败北 188/194 次);成长代理档 N=30:普通 40%/完美 80%;**预案「零成长必红档」预判被部分证伪**(棋盘武器库的克制输出〔魔法矢+1 克重甲/爆裂/攻击增益〕是预案生存链估算未计入的供给面),但成长批必要性判读不变(零成长读数低于 L1/L2 分带下缘、Boss 死亡占比过高、带域需成长后才可判读)——L3 行值维持 provisional-pending-growth,正式带归曲线调参批与 L3-A 同窗。**GP+商店房定稿提案交付**(design/research/proposal-gp-shop-room-2026-09-20.md,196 行 10 拍板点):格面池零修订(GP 走战斗掉落单渠道 普通 2/Boss 5 固定值)、节点图四类→五类加商店房(每关恰 1 中段必经位 L2 行集首落地)、三属性 flat 同族档(攻+1 首价 4/血上限+3 首价 2/护甲+1 首价 4,重复购买每档+2,三选一+刷新 2 GP 限 2 次,shop_roll 新随机流)、入关第三关参数账 攻 5–6/血 28–34/AC11 对照成长代理档、「抽象货币不做」红线判读不拦(GP=战斗之果非扫雷夹层)、验收 shop=off/on 双格分层——**十拍板点待用户逐条表态后进波次 2 实施批(A 表/B 代码/C 文档/D 验证四互斥文件集拆批清单已附)**。
116. **GP+商店房实施批(L3-A 玩家成长批)(2026-09-20,本会话——用户对 proposal-gp-shop-room-2026-09-20.md 六开放问题逐条裁决(关键推翻:GP 进第九型格面,格面池零修订推荐被否);plan 批四子代理接力(A 表/B 代码/C 文档/D 冒烟);worktree gp-shop-20260920)**:**裁决要点**——①GP 进九型:金币袋(gp_gain,power 3,weight 2,低概率惊喜获取、量与战斗掉落相当)+破财(gp_loss,power 2,weight 0 字典行,钱包 clamp ≥0);②掉落非固定值=均值±方差掷定(普通 3[2,4]/Boss 8[6,10]/精英 5[4,6] 预留行)+关级加值(L1 0/L2 1/L3 2)+关级价格加成(L1 0/L2 2/L3 4);③记账=完成战斗统一到账(battle_finished 载荷 +gp_dropped),战斗内格面兑付走战斗域缓冲 escrow(战斗开始清零随载荷上抛即弃)与钱包隔离;④商店房=Boss 前一位必经位(哈迪斯式,L2/L3 行集 Boss 重编号 8→9、(8,shop,next[9])、L1 零改动、必经位冒烟断言承载);⑤统一冒险加算区 {atk,max_hp,armor_class}——商店购买与关卡大奖励同账、经覆盖值通道生效、购血上限与当前生命同步 +Δ;⑥三商品全陈列**无刷新无三选一**(shop_roll 作废),定价按期望账制定(L2 商店前可得 ≈35–40 GP vs 三件首购 16/全购 26–34,取舍空间成立,账进 shop.tres 批注);⑦**数值与分层验收整体后置**(用户拍板"骨架先行、数值是肌肉":shop=off/on 双格 MC、带平移、换算率全归曲线调参批与既有三条目同窗);⑧「关卡内结构随机生成(哈迪斯/弹珠式)」登记 06 方向观察条目未立项。**落地面**——新表 shop.tres+ShopTable schema(四行族+域校验)、reward 九型批(CELL_TYPES+gp、字典十八格、正向合计 76)、level_table 词表五类+L2/L3 rewiring(陈旧 (·,8) Boss 行清理)、代码四件(rng_service gp_drop 流〔黄金向量 m=42 scope0=6226476397888000011/scope1=5031558177015478025 已登 registry〕、battle_orchestrator escrow+gp_dropped、level_runner 钱包三收口+商店兑付+加算区、config_db shop 加载+gp 效应豁免)、呈现五件(ShopOverlay 商店覆盖层/顶栏金币常驻/格面徽标 gp 金色键〔避开紫赤禁入带〕/选路图商店房词表/battle_narrator 台词)、playtest_logger(battle_end.gp_dropped+wallet 事件行)、GDD 八文档(关卡节点地图 v1.1 五类枚举+编排规则/冒险结构 v0.5 GP 冒险账本+统一加算区/战斗结算 v0.23 载荷+缓冲/地图战斗资源 v0.15 九型批/难度曲线 v0.18 杠杆登记+分层 defer 成文/元进度 v1.2 双币分工/03 v0.7/05 红线显式例外成文)、registry 六处(reward/shop_table 新条目/level_table/载荷/随机流+黄金向量/术语三词条)、06 GP 提案条目解决移除、提案文首裁决记录块。**验证**——冒烟 437 项 0 失败三闸全过(+60 项断言:GP 到账区间/escrow 隔离+破财 clamp/商店三收口+价格递增+余额拒/图不变式含 DFS 全路径必经穿店/九型字典+黄金向量);ui_probe/click_probe(商店层适配)/tune_probe 三探针 PASS。
117. **冒险结构实现批——三关正式序列生效(2026-09-21,本会话——按用户规划书 .zcode/plans/adventure-structure-batch-plan-2026-09-21.md T0–T9 顺序执行,规划书 §7 代拍板默认值七项直接采行;worktree adventure-structure-20260921,代码批 7ddfa4b+文档批 5709cd3)**:**批次定位**——切片从「三个可分别试玩的单关 demo」升级为「一局完整的三关 Roguelike 冒险」:冒险级状态(生命账本/GP 钱包/统一加算区/主种子)所有权自 LevelRunner 上交 AdventureRunner(core/adventure_runner.gd 新建,Core 层零 UI)。**落地面**——①AdventureTable 建表:adventure_table.gd(两字段域检 [0,999])+adventure.tres(10/10 provisional)+config_db 接线+冒烟表闸 +4;②LevelRunner 账本外置(06 接口漂移①②③⑥⑦清偿):hero_hp/gp 改属性落 _ledger 字典(冒险模式=共享账本,调用点零改动)、恢复数学抽成单一路径 rest_heal_amount(冒险模式经 _rest_handler=AdventureRunner.apply_rest——接口③)、start_level_external 冒险模式入口(零重播种零账本重置)、close_wallet 公开化(冒险模式收束清零归 AdventureRunner._settle;漂移⑥=不发明空清库接口、注释登记事实清库);③AdventureRunner:start_adventure 主种子恰一次(rng_service new_adventure 唯一正式调用方——registry 同批登记)/大奖励非末关恰兑付一次(上限提升读 db.adventure+血回满=新上限+钥石照记,末关不发提升)/三结局结算(completed/failed/abandoned)+摘要五件事/结算受理只算一次(state 守卫)/四件套接口正式承载/ADR-0001 S6 全 _forward_* 方法转发;④level_autoplay 增 run_adventure(三关连探自动驱动——_drive_level 从 run_level 抽取零行为变更)+encounter_max_hp_seq/wallet_events/reward_events/shop_gp_seq 采集面;⑤呈现层:首页 lvbtn 改「开始冒险(三关连探:地精洞窟→幽暗地域→灰矮人矿城)」、L2/L3 加「测试直入口」前缀保留、大奖励屏(复用 inter_overlay+Narrator.big_reward_text)+冒险结算屏(复用 BattleEndOverlay+Narrator.adventure_summary_text 摘要五件事);⑥冒烟新段 +45(437→482:表闸 +4/API 闸 +8/冒险验收 +33——表覆盖义务/满配通关/种子复现/大奖励恰 2 次与覆盖值传播/败北线两种子扫描〔零配 L1 败+半配 L2 败〕/重复宣布拒绝/GP 跨关延续〔gain= battles·clear 恰一次为末事件·L2 商店房余额>0〕/加算区同账〔商店+大奖励同账累加+回满新上限〕/恢复数学三读数/放弃两态);⑦文档:registry 三处(adventure_table 已建表/level_table 三关序列生效/rng_service new_adventure 调用权)、06 三条目解决移除(两关序列未生效/切片行进代码与定稿的接口漂移/冒险表未建表)、03 v0.8(L2/L3 状态行改正式序列已生效)、冒险结构 v0.6(实现批落地登记,正文零改动)、roadmap(时间线 09-20/09-21 两行+路线图第 4/5 项收口——存档批与元进度批挂点就位)、切片 README(主入口口径+修改记录条目)。**代拍板登记(规划书 §7 七项采行,待用户确认)**:①lvbtn 改开始冒险+L1 单关直入口取消(冒险首关即 L1);②大奖励屏复用 inter_overlay;③冒险结算屏复用 BattleEndOverlay;④钥石只在结算屏摘要展示(HUD 常驻归元进度批);⑤每关新建 LevelRunner;⑥不发明空清库接口;⑦放弃 API 预留+冒烟断言、无 UI 入口。**规划书冲突按代码为准三处(台账登记)**:①_pending 置位先于 emit——信号同步派发下大奖励处理器内立即 continue_to_next_level 需 _pending_next_level 已就位(T4 手动验证实证 advance_failed 后修正);②encounter_max_hp_seq 抓取点改 runner.effective_max_hp()——规划书指定 node_entered 时抓 encounter.hero_max_hp,但 encounter 字典在 begin_battle 内才赋值(node_entered 先于 begin 发出)读到空字典,effective_max_hp() = begin_battle 将收 hero_max_hp 同一表达式等价;③abandon() 补 state=running 守卫——规划书只写战斗中拒绝,补齐后结算后重复放弃亦拒绝(契约 7 结算受理只算一次的 abandoned 侧对齐)。**验证**——冒烟 482 项 0 失败三闸全过;四探针全过(ui_probe/tune/click 问题=0/layout 转储 exit 0——click 探针真实驱动了一局冒险失败终局屏);MC 零行为变更回归:T0 同款命令(20 局 L1 normal unique seed0=4200,基线格通关率 35%)重跑 10 行读数与基线逐位一致(diff 仅 _drive_level 抽取的堆栈帧行号与退出泄漏计数——非行为面);文档三工具与 master 基线一致(doc_link_audit 死链 7=7 既有欠账、链接 787→786 系本批删除一条待办指路链接、gdd_lint 8 文件既有、doc_term_audit 215 处/疑似现Line truncated
@@ -12,7 +12,7 @@
1. 用 Godot 4.7.2 打开本目录(含 `project.godot` 的文件夹)
2. 按 F5 运行(主场景 `presentation/main.tscn`)
3. **主入口:`▶ 第一关 · 地精洞窟`**——线性节点链:战→战→赏(随机主动道具)→战→战→疗(血量恢复点)→王(Boss 遗迹守卫)
3. **主入口:`▶ 开始冒险(三关连探:地精洞窟 → 幽暗地域 → 灰矮人矿城)`**——一条命走完三关(冒险结构实现批 2026-09-21:生命/GP/加算区跨关同账、非末关通关发大奖励〔上限提升+血回满+钥石〕、任意战斗倒下冒险终、结算屏出冒险摘要五件事);首关节点链:战→战→赏→战→战→疗→王。`▶ 测试直入口 · 第二关 / 第三关` 保留单关试玩模式
4. `测试用 · 角色数值调整板`:攻击/生命百分比加成(模拟天赋树解锁)+ 批量模拟(输出通关率/承伤期望/败北节点分布)
headless 冒烟(55 项断言,含关卡线性推进/满配通关/裸装败北/回放确定性,约 30 秒):
@@ -169,7 +169,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
## L2 试玩采集清单(分支节点图批 2026-09-17——关卡 2「幽暗地域」试玩观测项)
入口:首页"▶ 第二关 · 幽暗地域(分支节点图试玩——岔路口选下一跳)"(测试模式直入口,种子框同用;与关卡 1 行为一致:通关/败北回首页)。
入口:首页"▶ 测试直入口 · 第二关 · 幽暗地域(分支节点图试玩——岔路口选下一跳)"(测试模式直入口,种子框同用;正式序列走「开始冒险」主入口)。
- **选路行为**:战斗胜利/兑付确认后弹出选路覆盖层(整图 + 候选按钮)——玩家是否理解"零自动推进、每跳都要选";单候选节点(恢复点→编号 6)是否被读成"卡住"。
- **岔路使用率**:四处真实岔路的走向分布(入口 0→[1,2];战斗 1→[3,4];奖励 2→[4,5];深层 4→[6,7])——奖励点路线 vs 纯战斗路线的选择比例;整图渲染是否被实际用来规划(对比只点按钮的玩家)。
@@ -209,7 +209,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
5. **难度台阶可感知性**:D2 伏击兽/D4 Boss 相比 D0 蛆虫的变强是否有具体面孔(新敌种=新图像线索)。
6. **奖励供给体感**:可选类道具(刀/治疗包)获取频率与囤积感受(供给控制器批后每关 ≈4.5–6.5 件)。
试玩方式:F5 → 「▶ 第一关 · 地精洞窟」基线打一局 → 调整板 +100% 攻击再打一局(对照"数值提升→通关期望改善"的元进度体感)。
试玩方式:F5 → 「▶ 开始冒险」基线打一局(一条命三关连探——冒险结构实现批 2026-09-21 起,第一关读数即冒险首关) → 调整板 +100% 攻击再打一局(对照"数值提升→通关期望改善"的元进度体感)。
## 2026-09-12 试玩批次 1 → v3 数值批(F7 流程实践)
@@ -522,3 +522,11 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
- 结构面:SkillTable type 枚举 +debuff;池纪律 +每池妨害 ≤1;施放类 side 守卫(buff→enemy / debuff→hero,`validate_skill_pool_links` 增 status_table 显式入参——装载序修正);意图徽标 +妨害系 ☠(紫)。
- 验证:冒烟 306 项 0 失败(+新探针 `_smoke_debuff_rider`:毒爆行逐拍精确复算 / rider 键封闭 / 蛛网束缚落主角端到端 / 六项加载校验拒绝)+ 四探针 PASS;800 局模拟试玩(两关 × 20 种子 × 双画像)——L2 基线通关 90%→80%、死因全部集中 Boss(设计方向生效),L1 零行为变更回归一致(65%/85%)。
- 顺手收口:smoke_all `variant_pool(1, ["dungeon"])` → `"dungeon"`(并行 board_type 批签名 Array→String 的漏网同步——此前该断言被 SCRIPT ERROR 静默吞掉);analyze_playtest.py 反事实重算 +rider 识别。
## 2026-09-21 冒险结构实现批(三关正式序列生效——AdventureRunner + LevelRunner 账本外置 + AdventureTable 建表)
**主入口升级**:「▶ 开始冒险(三关连探:地精洞窟 → 幽暗地域 → 灰矮人矿城)」——一条命走完三关的完整 Roguelike 冒险;`▶ 测试直入口 · 第二关/第三关` 保留单关试玩模式,L1 单关直入口随批取消(冒险首关即 L1)。
**冒险级语义**(设计权威 = 冒险结构.md v0.6):主种子恰一次注入(冒险内不重播种,同种子同操作序列逐位可复现);生命账本跨关同账(战后回写、恢复点按比例封顶、大奖励回满);GP 钱包跨关延续、冒险收束清零;统一冒险加算区(商店购买与大奖励同账);非末关通关发大奖励(上限提升 +10 读 adventure.tres、血回满、钥石 +10),末关不发上限提升、钥石照记;任意战斗倒下 = 冒险失败;结算屏出冒险摘要五件事(打穿没有/通关几关/止步第几关/钥石合计/主种子)。
**验证**:冒烟 437→482 项 0 失败三闸全过(冒险表 +4 / API 闸 +8 / 冒险验收 +33:表覆盖义务、满配通关、种子复现、大奖励恰 2 次与覆盖值传播、败北线两种子扫描、重复宣布拒绝、GP 跨关延续、加算区同账、恢复数学、放弃两态);MC 回归零行为变更(run_level 旧路径同种子逐位一致)。
@@ -0,0 +1,224 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 三关连探冒险(地精洞窟 → 幽暗地域 → 灰矮人矿城)能否以一条命走完——
# 主种子恰一次注入、生命/GP/加算区跨关同账、大奖励非末关恰兑付一次、三结局摘要五件事成立(P0)
# Date: 2026-09-21(冒险结构实现批——设计权威 design/gdd/systems/冒险循环/冒险结构.md)
# 冒险结构编排器(Core 层零 UI 依赖——headless 可跑完整冒险):一局的关卡序列、生命账本、GP 冒险
# 账本、统一冒险加算区、关卡大奖励、三结局结算与主种子接线的唯一所有者。
#
# 接口契约四件套落点(冒险结构.md「与关卡节点地图的边界」——本类是正式侧承载):
# ① 开始关卡 = start_level_external(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接);
# ② 战斗结果上报 = runner 经共享账本属性直写(LevelRunner._on_battle_finished 回写 hero_hp 到
# _ledger——零直调战斗域,战后生命终值随账本回流本类);
# ③ 治疗封顶回报 = apply_rest(恢复点节点兑付时 LevelRunner 经 _rest_handler 回调——按比例回血并
# 封顶的执行归本类);
# ④ 关卡结束 = _on_level_finished(LevelRunner.level_finished 到达——结果 + 生命终值 + 节点位置)。
#
# 接口漂移清偿登记(06 清单①②③⑥⑦——LevelRunner 侧同批登记):①种子域归本类
# (start_adventure 调 rng.new_adventure 全冒险恰一次;LevelRunner 冒险模式不重播种);
# ②生命账本归本类(共享 _ledger);③恢复兑付执行归本类(_rest_handler);⑥收束先清库后宣布——
# 现行无关卡域库存(格面资源批 2026-09-15 后为空集),战斗结束 retire_mapping + despawn_all 先于
# battle_finished 即事实清库,不另设空接口;⑦失败结算归本类(LevelRunner 只上报结果)。
#
# 收束次序登记(大奖励兑付时点——冒险结构.md「关卡大奖励」):关卡节点地图先清库、后宣布关卡
# 结束(次序不可换);大奖励在宣布之后、下一关开始之前兑付——本类 _on_level_finished 兑付、
# continue_to_next_level 开始下一关,两步间为呈现层大奖励屏(冒烟/自动路径立即确认)。
# 结算受理只算一次(契约):state != "running" 的关卡结束宣布与放弃请求一律拒绝并留诊断,
# 账本与摘要零变化。
# 纪律:ADR-0001 S6——连 runner 信号一律 _forward_* 方法 Callable(禁 lambda 捕获 self 成环);
# Core 层禁 UI 引用;数值零字面量——level_clear_hp_bonus / keystone_per_level 只读 db.adventure,
# 基础上限只读 db.hero。
class_name AdventureRunner
extends RefCounted
signal level_started(level_index: int, display_name: String) # 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行
signal big_reward_paid(reward: Dictionary) # 大奖励兑付(非末关通关恰一次)——{from_level, hp_bonus, new_max_hp, keystone_gained, keystone_total};呈现层大奖励屏/冒烟立即确认
signal adventure_finished(summary: Dictionary) # 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子
# ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明;ADR-0001 S6 经 _forward_* 方法)----
signal node_entered(node: Dictionary, info: Dictionary)
signal choices_presented(candidates: Array)
signal step_row(row: Dictionary)
signal round_started(round_index: int)
signal board_changed()
signal wallet_changed(event: String, gp: int, extra: Dictionary)
var db
var rng
var state := "idle" # idle / running / settling(冒险结构.md 三态)
var ledger := {"hero_hp": 0, "gp": 0} # 冒险账本(生命 + GP 钱包——经 start_level_external 注入 runner 共享)
var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0} # 统一冒险加算区(商店购买与大奖励同一本账)
var purchased := {} # product_id → 已购档数(跨关延续、冒险收束清零——递增定价输入)
var level_index := 0 # 当前关卡序号(1 起)
var levels_total := 0 # 序列总关数(读 level 表行数)
var keystone := 0 # 钥石入账累计(摘要第三件事的账面)
var master_seed := 0 # 本局主种子(摘要第五件事)
var summary := {} # 最近一次结算的冒险摘要(idle 后保留——重开才新生成)
var _runner: LevelRunner # 当前关卡行进器(每关新建——关卡域状态天然隔离)
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
var _override := [0, 0] # TunePanel 百分比通道 [atk_pct, hp_pct](每关新 runner 都要应用)
func _init(db_ref, rng_ref) -> void:
db = db_ref
rng = rng_ref
# ---- 冒险生命周期 ----
# 冒险开始(主种子注入恰一次——全冒险唯一 rng.new_adventure 调用点;重开 = 全新一轮:
# 账本/加算区/已购档/钥石/摘要全重置,生命置满基础上限,从第 1 关起步)
func start_adventure(seed_val: int) -> bool:
if state != "idle":
push_error("[冒险结构] start_adventure 拒绝——state=%s(重开须待上一局结算完毕)" % state)
return false
master_seed = seed_val
rng.new_adventure(seed_val)
ledger = {"hero_hp": 0, "gp": 0}
bonus = {"atk": 0, "max_hp": 0, "armor_class": 0}
purchased = {}
keystone = 0
summary = {}
levels_total = db.level.rows.size()
level_index = 1
_pending_next_level = false
ledger["hero_hp"] = int(db.hero.max_hp) # 生命账本置满(基础上限读英雄表——加算区刚归零)
state = "running"
return _start_current_level()
# TunePanel 百分比通道(测试面板覆盖——每关新 runner 都要应用;正式侧归元进度)
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
_override = [atk_pct, hp_pct]
if _runner != null:
_runner.set_hero_override(atk_pct, hp_pct)
# 当前关卡行进器读面(呈现层/冒烟用——每关新建实例,调用方须在 level_started 后重取)
func runner() -> LevelRunner:
return _runner
# 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶;与 LevelRunner.rest_heal_amount
# 同式,同一公式两个落点是临时态,注释指认,统一归并归后续批)
func apply_rest(pct: int, hp: int, max_hp: int) -> int:
return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp)
# 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;呈现层大奖励屏 / 冒烟立即确认)
func continue_to_next_level() -> bool:
if not _pending_next_level:
push_error("[冒险结构] continue_to_next_level 拒绝——无待确认的关卡推进")
return false
_pending_next_level = false
level_index += 1
return _start_current_level()
# 主动放弃(仅非战斗期受理——战斗域单事务在飞;与失败同型结算,已获大奖励不追回。
# MVP 无界面入口,语义先行预留;结算受理只算一次——非 running 态拒绝)
func abandon() -> bool:
if state != "running":
push_error("[冒险结构] abandon 拒绝——state=%s(非进行中不可放弃)" % state)
return false
if _runner != null and _runner.current_battle() != null:
push_error("[冒险结构] abandon 拒绝——战斗进行中(战斗域单事务在飞)")
return false
_settle("abandoned")
return true
# ---- 关卡推进(私有——序列编排单一出口)----
func _start_current_level() -> bool:
_runner = LevelRunner.new(db, rng)
# core↔core 接线只许方法 Callable(ADR-0001 S6——lambda 捕获 self 会延长 RefCounted 生命周期乃至成环)
_runner.node_entered.connect(_forward_node_entered)
_runner.choices_presented.connect(_forward_choices_presented)
_runner.step_row.connect(_forward_step_row)
_runner.round_started.connect(_forward_round_started)
_runner.board_changed.connect(_forward_board_changed)
_runner.wallet_changed.connect(_forward_wallet_changed)
_runner.level_finished.connect(_on_level_finished)
_runner.set_hero_override(int(_override[0]), int(_override[1]))
# 接口①开始关卡:生命经共享账本交接(账本当前值即开局生命——冒险开始置满 / 大奖励回满已写好)
var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased,
Callable(self, "apply_rest"))
if ok:
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
return ok
# 接口④关卡结束(结果 + 生命终值 + 节点位置——胜负皆报;结算受理只算一次)
func _on_level_finished(payload: Dictionary) -> void:
if state != "running":
push_error("[冒险结构] 重复关卡结束宣布拒绝——state=%s(账本与摘要零变化)" % state)
return
var result := String(payload.get("result", ""))
if result == "defeat":
ledger["hero_hp"] = 0 # 倒下记 0(生命账本战后回写口径)
_settle("failed")
return
if result != "victory":
push_error("[冒险结构] 关卡结束结果出域: %s" % result)
return
if level_index < levels_total:
# 大奖励(非末关通关恰一次):上限提升写加算区 → 血回满 = 新上限 → 钥石入账(顺序即契约)
var hp_bonus: int = int(db.adventure.level_clear_hp_bonus)
bonus["max_hp"] = int(bonus.get("max_hp", 0)) + hp_bonus
ledger["hero_hp"] = int(db.hero.max_hp) + int(bonus["max_hp"])
keystone += int(db.adventure.keystone_per_level)
# 置位先于 emit(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位)
_pending_next_level = true
big_reward_paid.emit({
"from_level": level_index,
"hp_bonus": hp_bonus,
"new_max_hp": int(db.hero.max_hp) + int(bonus["max_hp"]),
"keystone_gained": int(db.adventure.keystone_per_level),
"keystone_total": keystone,
})
return
# 末关通关:钥石照记、不发上限提升(没有下一战可受益)——冒险完成结算
keystone += int(db.adventure.keystone_per_level)
_settle("completed")
func _settle(result: String) -> void:
# 冒险摘要五件事:打穿没有 / 通关几关 / 止步第几关 / 钥石入账合计 / 本局主种子
summary = {
"result": result,
"cleared": result == "completed",
"levels_cleared": levels_total if result == "completed" else level_index - 1,
"stopped_at": 0 if result == "completed" else level_index,
"keystone_total": keystone,
"master_seed": master_seed,
}
if _runner != null:
_runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现)
state = "settling"
adventure_finished.emit(summary)
state = "idle"
# ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明)----
func _forward_node_entered(node: Dictionary, info: Dictionary) -> void:
node_entered.emit(node, info)
func _forward_choices_presented(candidates: Array) -> void:
choices_presented.emit(candidates)
func _forward_step_row(row: Dictionary) -> void:
step_row.emit(row)
func _forward_round_started(ri: int) -> void:
round_started.emit(ri)
func _forward_board_changed() -> void:
board_changed.emit()
func _forward_wallet_changed(event: String, gp_val: int, extra: Dictionary) -> void:
wallet_changed.emit(event, gp_val, extra)
@@ -0,0 +1 @@
uid://lem5anpkcd0p
@@ -21,6 +21,7 @@ const TABLE_MANIFEST := {
"level": "level.tres",
"difficulty": "difficulty.tres",
"shop": "shop.tres", # GP+商店房实施批 2026-09-21(表与 schema 落籍归并行数据面——本批只接线加载与消费)
"adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点)
}
var action_economy: ActionEconomyTable
@@ -37,6 +38,7 @@ var difficulty: DifficultyTable
# GP+商店房实施批(2026-09-21):ShopTable 暂持无类型注记——schema class_name 落籍归并行数据面,
# 落籍后可收紧为 `var shop: ShopTable`(消费面一律走下方 shop_* 访问器,禁直读行集)
var shop = null
var adventure: AdventureTable
var is_ready := false
var _diagnostics: Array = [] # {severity, category, message}
@@ -119,6 +121,7 @@ func load_tables(root: String, collect_only: bool) -> bool:
level = loaded.get("level")
difficulty = loaded.get("difficulty")
shop = loaded.get("shop")
adventure = loaded.get("adventure")
is_ready = not has_errors
return not has_errors
@@ -17,6 +17,18 @@
# 加算区 bonus{atk,max_hp,armor_class}(商店购买与关卡大奖励共用账本——战斗开始参数 = 基础表值 +
# 加算区,TunePanel 百分比在加算之后乘)+ 商店房节点兑付(开覆盖层/购买出账/离开回选路确认——
# 复用 continue_after_node 兑付确认纪律,零自动推进)。
# 冒险结构实现批(2026-09-21)——接口漂移清偿登记(06 清单「切片行进代码与定稿的接口漂移」①②③⑥⑦):
# ① 种子域归冒险结构:start_level 旧路径(单关测试模式)保留自调 rng.new_adventure;新路径
# start_level_external(冒险模式)不重播种——主种子注入恰一次归 AdventureRunner.start_adventure。
# ② 生命账本归冒险结构:hero_hp/gp 改属性落 _ledger 字典——冒险模式经 start_level_external 注入
# AdventureRunner 共享账本(Dictionary 引用共享),单关模式 = 本实例私有账本;战斗结果上报语义 =
# runner 经共享账本属性直写(接口②——战后生命终值已在本类 _on_battle_finished 回写账本)。
# ③ 恢复数学抽成单一路径 rest_heal_amount——冒险模式经 _rest_handler 回调(AdventureRunner.apply_rest,
# 接口③治疗封顶回报执行体归冒险结构);单关模式默认内置。公式逐位一致,零行为变更。
# ⑥ 收束先清库后宣布——现行无关卡域库存(格面资源批 2026-09-15 可选类删除后为空集),战斗结束
# retire_mapping + despawn_all 先于 battle_finished 发出即事实清库,不另设空接口(假契约)。
# ⑦ 倒下的失败结算归冒险结构:本类只发 level_finished(每战结果上报语义),失败结算由
# AdventureRunner._on_level_finished 执行;GP 冒险收束清零同步归冒险结构(本类冒险模式不关钱包)。
class_name LevelRunner
extends RefCounted
@@ -32,9 +44,20 @@ var db
var rng
var level_row: Dictionary = {}
var node_index := -1
var hero_hp := 0
var gp := 0 # GP 冒险钱包(局内货币——冒险开始 = 0、收束清零;不入存档不入摘要)
var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0} # 统一冒险加算区(冒险开始全 0)——商店购买与关卡大奖励 hp 上限提升写同一本账(大奖励在切片序列未生效,契约位:大奖励 hp 上限提升写本加算区,随正式序列批接线)
# ---- 账本载体(冒险结构实现批——漂移②清偿)----
# 冒险模式 = AdventureRunner 共享字典(Dictionary 引用共享——hero_hp/gp 属性读写直接落冒险账本);
# 单关模式 = 本实例私有账本(start_level 重建,既有单关语义逐位保留)。
var _ledger: Dictionary = {"hero_hp": 0, "gp": 0}
var _rest_handler: Callable # 治疗封顶回报执行体(默认内置 rest_heal_amount;冒险模式 = AdventureRunner.apply_rest——接口③)
var _external := false # 冒险模式标记(钱包收束时机与播种语义分叉点)
# hero_hp / gp 属性化(既有读写点零改动——值类型经属性落 _ledger)
var hero_hp: int:
get: return int(_ledger["hero_hp"])
set(v): _ledger["hero_hp"] = v
var gp: int: # GP 冒险钱包(局内货币——冒险开始 = 0、收束清零;不入存档不入摘要)
get: return int(_ledger["gp"])
set(v): _ledger["gp"] = v
var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0} # 统一冒险加算区(冒险开始全 0)——商店购买与关卡大奖励写同一本账(冒险模式 = AdventureRunner 共享字典;大奖励接线归 AdventureRunner._on_level_finished)
var _shop_purchased := {} # product_id → 已购档数(现价重算输入;冒险开始清零)
# 协作对象私有(E1/E3 封装收口 2026-09-13):settlement 读取经 current_battle() 成文出口
var _settlement = null # 当前战斗编排器(关卡结束置 null)
@@ -87,6 +110,11 @@ func restore_hero_base() -> void:
# ---- 关卡生命周期 ----
func start_level(level_index: int, seed_val: int) -> bool:
# 单关测试模式(旧路径——既有语义一字不改):私有账本、内置恢复数学、自调 new_adventure(漂移①
# 单关=冒险的历史简化在测试模式保留;正式种子域归 AdventureRunner.start_adventure)
_external = false
_rest_handler = Callable(self, "rest_heal_amount")
_ledger = {"hero_hp": 0, "gp": 0}
var row := _find_level_row(level_index)
if row.is_empty():
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
@@ -116,6 +144,42 @@ func start_level(level_index: int, seed_val: int) -> bool:
return _enter_node(entry)
# 冒险结构接口①:开始关卡(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接;
# 种子域归冒险结构:本路径不重播种;账本归冒险结构:本路径不重置 gp/bonus/_shop_purchased,
# 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在 ledger 写好)
# 流程与 start_level 同款(_find_level_row → 状态初始化 → _graph_mode 探测 → _enter_node(entry)),
# 差异仅在账本/加算区/已购档/恢复执行体的注入与零重置。
func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dictionary,
purchased_ref: Dictionary, rest_handler: Callable) -> bool:
var row := _find_level_row(level_index)
if row.is_empty():
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
return false
_external = true
_ledger = ledger
bonus = bonus_ref
_shop_purchased = purchased_ref
_rest_handler = rest_handler
level_row = row
node_index = -1
level_result = ""
_awaiting_choice = false
_candidates = []
visited = []
_graph_mode = false
for node in nodes():
if node.has("next"):
_graph_mode = true
break
_running = true
var entry := _entry_node()
if entry.is_empty():
push_error("[关卡节点地图] 入口节点缺失(无入边唯一节点——图合法性校验应已拦截)")
_running = false
return false
return _enter_node(entry)
func _find_level_row(level_index: int) -> Dictionary:
for row in db.level.rows:
if int(row.level_index) == level_index:
@@ -281,10 +345,12 @@ func _on_battle_finished(payload: Dictionary) -> void:
hero_hp = maxi(1, int(payload.get("hero_hp", 0))) # despawn 前捕获值(载荷自包含)
_gain_wallet(int(payload.get("gp_dropped", 0))) # 战斗完成入账(clamp ≥0——破财可把 escrow 打负,钱包入账时钳制)
if String(node.get("type", "")) == "boss" or result == "defeat":
# Boss 胜利 = 关卡通关;任意战斗败北 = 关卡失败(Roguelike——冒险即终,重开归 冒险结构/元进度)
# Boss 胜利 = 关卡通关;任意战斗败北 = 关卡失败(漂移⑦清偿——每战结果上报冒险结构,
# 失败结算归 AdventureRunner;本类只发 level_finished)
level_result = "victory" if result == "victory" else "defeat"
var hp_left := hero_hp if result == "victory" else 0
_close_wallet() # 冒险收束(通关/倒下)→ GP 清零(局内货币生命周期终点)
if not _external:
close_wallet() # 单关模式:关卡收束即冒险收束(GP 清零);冒险模式归 AdventureRunner._settle
_running = false
_settlement = null
level_finished.emit({"result": level_result, "hp": hp_left, "node_index": node_index,
@@ -295,11 +361,17 @@ func _on_battle_finished(payload: Dictionary) -> void:
_present_choices()
# 恢复数学单一路径(漂移③清偿——同一公式两个落点:本体内置默认 + AdventureRunner.apply_rest,
# 统一归并归后续批):floor(上限 × 比例 ÷ 100) 后封顶缺口;冒险结构 GDD 公式逐位一致
func rest_heal_amount(pct: int, hp: int, max_hp: int) -> int:
return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp)
func _apply_rest() -> int:
var pct: int = int(level_row.get("rest_heal_pct", 0))
var mhp := effective_max_hp()
var heal := int(floor(float(mhp) * float(pct) / 100.0))
var actual: int = mini(heal, mhp - hero_hp)
# 接口③治疗封顶回报:单关模式 = 内置 rest_heal_amount;冒险模式 = AdventureRunner.apply_rest
#(执行归冒险结构——回报比例、由其按封顶公式回写共享账本后再由本类叠加)
var actual: int = int(_rest_handler.call(pct, hero_hp, effective_max_hp()))
hero_hp += actual
return actual
@@ -355,8 +427,9 @@ func _gain_wallet(amount: int) -> void:
wallet_changed.emit("gain", gp, {"amount": amount})
# 冒险收束清零(通关/倒下——局内货币生命周期终点,不入存档不入摘要)
func _close_wallet() -> void:
# 冒险收束清零(通关/倒下/放弃——局内货币生命周期终点,不入存档不入摘要;public——冒险模式由
# AdventureRunner._settle 在冒险收束时调用,单关模式 = 关卡收束即冒险收束)
func close_wallet() -> void:
gp = 0
wallet_changed.emit("clear", 0, {})
@@ -0,0 +1,23 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 三关连探冒险(地精洞窟 → 幽暗地域 → 灰矮人矿城)能否以一条命走完,通关兑付的大奖励
# 与钥石合计在冒险摘要里成立(P0)
# Date: 2026-09-21(冒险结构实现批——AdventureTable 建表,registry adventure_table 条目 2026-09-15 登记)
# 数据配置 AdventureTable schema(冒险结构 关卡大奖励行值;单行全局,Rule 4 豁免——HeroTable/SettlementTable 先例)。
# 两字段 knob(调参表归 冒险结构;数值所有权归 难度曲线与Boss 曲线调参批,全部 provisional):
# level_clear_hp_bonus = 关卡大奖励 HP 上限提升(写统一冒险加算区 max_hp 轴——与商店购买同一本账,
# 经 begin_battle 覆盖值通道对后续每战生效;值锚 = 难度曲线与Boss 区间平移换算率,上调须重扫通关率带)。
# keystone_per_level = 每通关一关钥石入账值(末关通关照记;消费方 元进度——本表只记账不做消费)。
class_name AdventureTable
extends Resource
@export var level_clear_hp_bonus: int = 10 # [0,999] knob——关卡大奖励 HP 上限提升
@export var keystone_per_level: int = 10 # [0,999] knob——每通关一关钥石入账
func validate() -> Array:
var errs: Array = []
if level_clear_hp_bonus < 0 or level_clear_hp_bonus > 999:
errs.append({"category": "domain", "message": "level_clear_hp_bonus 越域 [0,999]: %d" % level_clear_hp_bonus})
if keystone_per_level < 0 or keystone_per_level > 999:
errs.append({"category": "domain", "message": "keystone_per_level 越域 [0,999]: %d" % keystone_per_level})
return errs
@@ -0,0 +1 @@
uid://cmg04glgweakh
@@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="AdventureTable" load_steps=2 format=3]
[ext_resource type="Script" path="res://data/schemas/adventure_table.gd" id="1"]
[resource]
script = ExtResource("1")
level_clear_hp_bonus = 10
keystone_per_level = 10
; 冒险结构实现批(2026-09-21 建表——registry adventure_table 条目同批注记更新):
; 关卡大奖励三件套的数值半边(上限提升)+ 钥石每关入账值;单行全局 Rule 4 豁免(HeroTable/SettlementTable 先例)。
; 大奖励 = 非末关通关恰一次:上限提升(写统一冒险加算区 max_hp 轴)+ 血回满(= 新上限)+ 钥石入账;
; 末关通关不发上限提升、钥石照记(设计意图:奖励只朝下一战生效)。钥石消费方 = 元进度(本表只记账)。
; 两值全部 provisional-pending-曲线调参批(值锚与上调约束归 难度曲线与Boss)。
@@ -82,3 +82,23 @@ static func boss_name(runner, db) -> String:
var row: Dictionary = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)))
return String(row.get("display_name", ""))
return "遗迹守卫"
# 大奖励屏文案(冒险结构实现批——兑付三件:上限提升/血回满/钥石入账;读数 = big_reward_paid 载荷快照)
static func big_reward_text(reward: Dictionary) -> String:
return "第 %d 层通过!大奖励:生命上限 +%d(新上限 %d),生命回满,钥石 +%d(累计 %d)" % [
int(reward.get("from_level", 0)), int(reward.get("hp_bonus", 0)),
int(reward.get("new_max_hp", 0)), int(reward.get("keystone_gained", 0)),
int(reward.get("keystone_total", 0))]
# 冒险结算屏文案(摘要五件事:打穿没有 / 通关几关 / 止步第几关 / 钥石合计 / 主种子)
static func adventure_summary_text(summary: Dictionary) -> String:
var verdict := "冒险失败 · 未打穿"
if bool(summary.get("cleared", false)):
verdict = "冒险完成 · 已打穿"
elif String(summary.get("result", "")) == "abandoned":
verdict = "冒险放弃 · 未打穿"
return "%s\n通关 %d 关 · 止步第 %d 层 · 钥石合计 %d · 主种子 %d" % [
verdict, int(summary.get("levels_cleared", 0)), int(summary.get("stopped_at", 0)),
int(summary.get("keystone_total", 0)), int(summary.get("master_seed", 0))]
@@ -29,7 +29,9 @@ const DebugSessionScript := preload("res://presentation/debug_session.gd")
var db
var settlement = null # 当前战斗编排器(关卡与调试模式统一读它)
var runner = null # 关卡节点地图 图版推进器(完整关卡模式)
var runner = null # 关卡节点地图 图版推进器(完整关卡模式;冒险模式 = 每关重取 adventure.runner())
var adventure = null # 冒险结构编排器(冒险结构实现批——三关连探;转发信号照 runner 接线原样对接)
var _adventure_mode := false # 冒险模式标记(inter 层确认分派与终局分派)
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
var _level_active := false
var _seed_edit: LineEdit
@@ -200,6 +202,9 @@ func _input_locked() -> bool:
func _start_full_level(level_index: int = 1) -> void:
_start_overlay.visible = false
if level_index == 1:
_start_adventure() # 冒险首关即 L1——正式序列入口(冒险结构实现批)
return
# E4:加成经 runner.set_hero_override → encounter 参数入战斗(零表写——无双写叠加隐患)
runner = LevelRunner.new(db, get_node("/root/RngService"))
runner.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct())
@@ -211,6 +216,7 @@ func _start_full_level(level_index: int = 1) -> void:
runner.level_finished.connect(_on_level_finished)
runner.wallet_changed.connect(_on_wallet_changed)
_level_active = true
_adventure_mode = false
_reset_run_scope()
log_view.clear()
var seed_val := _seed_value()
@@ -223,6 +229,40 @@ func _start_full_level(level_index: int = 1) -> void:
tune_panel.atk_pct() > 0 or tune_panel.hp_pct() > 0))
# 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接;
# 信号连接只在组合根——ADR-0001)
func _start_adventure() -> void:
adventure = AdventureRunner.new(db, get_node("/root/RngService"))
_adventure_mode = true
adventure.step_row.connect(_on_step_row)
adventure.round_started.connect(_on_round_started)
adventure.board_changed.connect(_on_board_changed)
adventure.node_entered.connect(_on_runner_node)
adventure.choices_presented.connect(_on_choices_presented)
adventure.wallet_changed.connect(_on_wallet_changed)
adventure.big_reward_paid.connect(_on_big_reward)
adventure.adventure_finished.connect(_on_adventure_finished)
adventure.level_started.connect(_on_adventure_level_started)
_level_active = true
_reset_run_scope()
log_view.clear()
var seed_val := _seed_value()
logger.start_session(seed_val)
adventure.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct())
if not adventure.start_adventure(seed_val):
_fatal_screen("冒险启动失败——见输出日志")
return
# 每关开始(冒险模式):重取 runner 实例 + 图条刷新 + 层名行(L1 与单关模式同序——层名行随首战横幅后到达)
func _on_adventure_level_started(level_index: int, display_name: String) -> void:
runner = adventure.runner()
strip_view.refresh(runner)
log_view.append_bb(Narrator.level_open_line(display_name, int(adventure.master_seed),
int(db.hero.atk), int(db.hero.max_hp),
tune_panel.atk_pct() > 0 or tune_panel.hp_pct() > 0))
func _reset_run_scope() -> void:
# 每 run 一次的提示去重与轮内计数(弃图刷新遥测轮内序号)
_hints_shown = {}
@@ -231,6 +271,9 @@ func _reset_run_scope() -> void:
func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
# 冒险模式:runner 逐关更替——每次节点事件重取(单关模式幂等);须先于 board_changed 回退读面
if _adventure_mode and adventure != null:
runner = adventure.runner()
var t := String(node.get("type", ""))
if t == "battle" or t == "boss":
settlement = runner.current_battle() # 呈现层统一读 settlement(与调试模式共用接线)
@@ -280,8 +323,41 @@ func _on_level_finished(payload: Dictionary) -> void:
_refresh_presentations()
# ---- 冒险模式终局面(大奖励屏 / 冒险结算屏——冒险结构实现批)----
# 大奖励兑付(非末关通关恰一次):复用 _inter_overlay(label + 确认按钮,零新增交互范式);
# 文案装配归 Narrator
func _on_big_reward(reward: Dictionary) -> void:
_inter_label.text = Narrator.big_reward_text(reward)
_inter_overlay.visible = true
_refresh_presentations()
# 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事进文案
func _on_adventure_finished(summary: Dictionary) -> void:
_level_active = false
var result: String = String(summary.get("result", ""))
settlement = null
route_overlay.visible = false # 终局兜底(选路口非 Boss 节点才有——防御性收层)
_sync_settlement_visuals() # → 全关
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④)
stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面)
if result == "completed":
_end_overlay.open_victory(Narrator.adventure_summary_text(summary),
"返回冒险", board_view.get_global_rect())
else:
_end_overlay.open_defeat(Narrator.adventure_summary_text(summary),
"返回", board_view.get_global_rect())
logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷)
if runner != null:
runner.restore_hero_base()
strip_view.refresh(runner)
_open_modal(_end_overlay)
_refresh_presentations()
func _on_battle_end_continue() -> void:
# 终局出口:返回开始界面(切片无下一关数据面——NodeOverlay/关卡结算屏归相邻域 spec)
# 终局出口:返回开始界面(冒险摘要留档试玩日志——钥石持久化归存档/元进度批)
_close_modal(_end_overlay)
_end_overlay.close()
_debug.reset()
@@ -289,6 +365,8 @@ func _on_battle_end_continue() -> void:
runner.restore_hero_base()
runner = null
settlement = null
adventure = null
_adventure_mode = false
tune_panel.reset_sliders()
strip_view.refresh(null)
_start_overlay.visible = true
@@ -328,6 +406,11 @@ func _on_debug_battle_ready(settlement_ref, _level: int, _carry: int) -> void:
func _on_inter_next_pressed() -> void:
_inter_overlay.visible = false
if _adventure_mode:
# 冒险模式:大奖励屏确认 → 进下一关(AdventureRunner 序列推进——零自动推进的冒险级停点)
if adventure != null and not adventure.continue_to_next_level():
_fatal_screen("关卡推进失败——见输出日志")
return
if not _debug.next_layer():
_fatal_screen("战斗启动失败——见输出日志")
@@ -120,11 +120,12 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary:
seed_edit.custom_minimum_size = Vector2(120, 0)
seed_row.add_child(seed_edit)
var lvbtn := UiKit.button("▶ 第一关 · 地精洞窟(战×2 → 奖励点 → 战×2 → 恢复 → Boss)")
# 冒险结构实现批(2026-09-21):lvbtn = 正式「开始冒险」入口(三关序列);L2/L3 保留测试直入口
var lvbtn := UiKit.button("▶ 开始冒险(三关连探:地精洞窟 → 幽暗地域 → 灰矮人矿城)")
box.add_child(lvbtn)
var lv2btn := UiKit.button("▶ 第二关 · 幽暗地域(分支节点图试玩——岔路口选下一跳)")
var lv2btn := UiKit.button("▶ 测试直入口 · 第二关 · 幽暗地域(分支节点图试玩——岔路口选下一跳)")
box.add_child(lv2btn)
var lv3btn := UiKit.button("▶ 第三关 · 灰矮人矿城(测试直入口——基线红档属预期,配合调整板加成试玩)")
var lv3btn := UiKit.button("▶ 测试直入口 · 第三关 · 灰矮人矿城(基线红档属预期,配合调整板加成试玩)")
box.add_child(lv3btn)
var tunebtn := UiKit.button("测试用 · 角色数值调整板(模拟天赋树属性提升)")
box.add_child(tunebtn)
@@ -11,6 +11,10 @@
# 2026-09-17 分支节点图批:选路适配(零自动推进)——兑付确认/战斗胜利后 runner 进选路态,
# 驱动循环按 route 策略显式 pick_next_node;route = "min"/"max"(候选节点编号最小/最大——
# 确定性路径轴,零随机流自开;默认 "min" 既有调用方签名兼容)。
# 2026-09-21 冒险结构实现批:run_adventure 三关连探驱动(AdventureRunner——主种子恰一次、
# 大奖励立即确认 continue_to_next_level、adventure_finished 置 result/summary;外层每关重取
# runner 实例经 _drive_level 复用单关驱动)。run_level 驱动段抽成 _drive_level——签名与
# 返回结构零变化,行为逐位不变(MC 同种子对照闸承载)。
# 运行前提:db 已 load_tables;每个 run_level 全新 RngService(同种子 ⇒ 全程逐步可回放)。
# 实现注:信号处理体全部为静态方法 + bind 传状态——GDScript 多行 lambda 内嵌 match/控制流的解析不可靠。
extends RefCounted
@@ -46,6 +50,22 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct:
if own_session:
logger.close_session()
return st
_drive_level(runner, st, logger, policy, route)
if st.result == "":
st.result = "timeout"
st.timeout = true
for v in runner.visited_ids():
st.visited_ids.append(int(v))
runner.restore_hero_base()
st.log_path = logger.log_path()
if own_session:
logger.close_session()
return st
# 单关驱动段(run_level 抽取——零自动推进停点 + 战斗自动玩家;policy 经 st 入 _autoplay_battle)
static func _drive_level(runner, st: Dictionary, logger, policy: String, route: String) -> void:
st["policy"] = policy
var guard_battles := 20
var guard_actions := 200
while st.result == "" and guard_battles > 0 and guard_actions > 0:
@@ -60,18 +80,110 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct:
if this_settlement == null:
break # level_finished 已发(settlement 置 null)而信号尚未置 result 的防御
_autoplay_battle(runner, this_settlement, st, logger)
# 三关连探冒险驱动(冒险结构实现批 2026-09-21)——AdventureRunner 编排:主种子恰一次
# (start_adventure 唯一 new_adventure 调用点)、大奖励立即确认、冒险终局置 result/summary。
# 返回结构 = run_level 同族 + summary / wallet_events / encounter_max_hp_seq / reward_events /
# visited_ids(全冒险节点行进序列——跨关累计,非末关 runner 的 visited 随实例更替,此处为唯一全量面)。
static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_rows: bool = false,
logger = null, policy: String = "normal", route: String = "min") -> Dictionary:
var own_session := logger == null
if own_session:
logger = load("res://presentation/playtest_logger.gd").new()
logger.start_session(seed_val)
logger.ap_per_round = int(db.action_economy.ap_per_round)
logger.log_run({"seed": seed_val, "adventure": true, "atk_pct": atk_pct, "hp_pct": hp_pct, "policy": policy, "route": route})
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var adr = load("res://core/adventure_runner.gd").new(db, rng)
var st := {
"result": "", "hp_left": 0, "battles": 0, "rounds": 0, "hero_dmg": 0,
"rest_healed": 0, "hp_before_rest": -1,
"defeat_node_index": -1, "defeat_node_type": "",
"boss_depth": -1, "hp_at_boss": -1, "rows": [], "timeout": false,
"log_path": "", "policy": policy, "refreshes": 0, "mine_hits": 0,
"visited_ids": [], "choices": 0,
"summary": {}, "wallet_events": [], "encounter_max_hp_seq": [], "reward_events": [],
"shop_gp_seq": [], # 商店房到达余额序(逐关 1 次——GP 跨关延续验收取数面)
}
adr.step_row.connect(_on_step_row.bind(adr, st, collect_rows))
adr.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger))
adr.wallet_changed.connect(_on_adventure_wallet.bind(st, logger))
adr.big_reward_paid.connect(_on_adventure_reward.bind(st, adr))
adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger))
adr.set_hero_override(atk_pct, hp_pct)
if not adr.start_adventure(seed_val):
st.result = "start_failed"
if own_session:
logger.close_session()
return st
var guard_drives := 10 # 每次外层迭代 = 驱动一关(关卡更替由 big_reward 确认与 runner 重取承载)
while st.result == "" and guard_drives > 0:
guard_drives -= 1
_drive_level(adr.runner(), st, logger, policy, route)
if st.result == "":
st.result = "timeout"
st.timeout = true
for v in runner.visited_ids():
st.visited_ids.append(int(v))
runner.restore_hero_base()
if adr.runner() != null:
adr.runner().restore_hero_base()
st.log_path = logger.log_path()
if own_session:
logger.close_session()
return st
# 冒险模式节点采集(_on_node_entered 的 AdventureRunner 门面版——runner 逐关更替,经 adr.runner() 重取;
# encounter_max_hp_seq 抓 effective_max_hp:node_entered 先于 begin_battle,encounter 尚为空字典,
# 该值 = begin_battle 将收到 hero_max_hp 同一表达式〔规划书抓取点与代码事实冲突,按代码为准已登记〕)
static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr, st: Dictionary, logger) -> void:
var r = adr.runner()
var t := String(node.get("type", ""))
st.visited_ids.append(int(r.node_id_of(node)))
if t == "battle" or t == "boss":
st.battles += 1
st.encounter_max_hp_seq.append(int(r.effective_max_hp()))
if logger != null and r.current_battle() != null:
logger.attach_battle(r.current_battle())
if t == "boss":
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)
elif t == "rest":
st.rest_healed = int(info.get("healed", 0))
st.hp_before_rest = int(info.get("hp", 0)) - st.rest_healed
r.continue_after_node()
elif t == "shop":
st.shop_gp_seq.append(int(info.get("gp", -1)))
r.continue_after_node() # 商店房零购离开(自动路径直通 Boss——购买行为归冒烟专项/真人试玩)
if logger != null:
logger.log_node(node, info, r.node_index)
# 冒险钱包事件采集(wallet 行族——GP 跨关延续与冒险收束 clear 的验收取数面;同日志合规转发)
static func _on_adventure_wallet(event: String, gp_val: int, extra: Dictionary, st: Dictionary, logger) -> void:
st.wallet_events.append({"event": String(event), "gp": int(gp_val),
"amount": int(extra.get("amount", 0)), "product": String(extra.get("product", ""))})
if logger != null:
logger.log_wallet(event, gp_val, extra)
# 大奖励立即确认(非末关通关 → continue_to_next_level 进下一关——自动路径无大奖励屏停点)
static func _on_adventure_reward(reward: Dictionary, st: Dictionary, adr) -> void:
st.reward_events.append(reward.duplicate(true))
if not adr.continue_to_next_level():
st.result = "advance_failed"
# 冒险终局(completed/failed/abandoned——result 载 summary.result,外层驱动循环随之收敛)
static func _on_adventure_finished(summary: Dictionary, st: Dictionary, logger) -> void:
st.summary = summary.duplicate(true)
st.result = String(summary.get("result", ""))
if logger != null:
logger.log_level_end(summary)
# 路径轴策略(确定性——候选编号最小/最大;min 侧 = 岔路恒取小编号支)。
# 编号取 runner.next_candidate_ids()(编号空间解析单一出口——单链下标 / 图 node_depth)
static func _route_pick(runner, route: String) -> int:
@@ -98,9 +98,251 @@ func _run_core_smoke(db, rng) -> void:
_smoke_shop_room()
_smoke_shop_graph()
_smoke_nine_types_gp_stream()
_smoke_adventure_table()
_smoke_adventure_api()
_smoke_adventure()
_smoke_save()
# ---- 冒险结构实现批冒烟(2026-09-21——AdventureTable 建表 + AdventureRunner + 冒险验收断言)----
# ① 冒险表(单行全局 Rule 4 豁免——建表与接线闸)
func _smoke_adventure_table() -> void:
print("--- 冒险表(AdventureTable 建表——大奖励/钥石行值单点)---")
check(_db.adventure != null, "db.adventure 已加载(adventure.tres 入 manifest)")
if _db.adventure == null:
return
check(_db.adventure.validate().is_empty(), "AdventureTable.validate() 零违例(两字段域检在案)")
var hp_bonus: int = int(_db.adventure.level_clear_hp_bonus)
var ks: int = int(_db.adventure.keystone_per_level)
check(hp_bonus >= 0 and hp_bonus <= 999, "level_clear_hp_bonus ∈ [0,999](%d)" % hp_bonus)
check(ks >= 0 and ks <= 999, "keystone_per_level ∈ [0,999](%d)" % ks)
# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 放弃两态)
func _smoke_adventure_api() -> void:
print("--- 冒险结构 API 闸(重复开始拒绝 · 放弃两态受理)---")
var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(42))
adr.set_hero_override(400, 200) # 首战速胜档(放弃的非战斗受理要走到选路态)
check(adr.start_adventure(42), "冒险启动受理(主种子注入恰一次——state=running)")
check(not adr.start_adventure(43), "进行中重复 start_adventure 拒绝(重开须待结算完毕)")
check(adr.runner().current_battle() != null, "首战在飞(接口①开始关卡直达战斗节点)")
check(not adr.abandon(), "战斗中 abandon 拒绝(战斗域单事务在飞)")
# 驱动首战到胜利 → 选路态(非战斗期)→ abandon 受理
var autoplay = load("res://smoke/level_autoplay.gd")
var battle = adr.runner().current_battle()
var st := {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"}
var guard := 30
while adr.runner().current_battle() == battle and guard > 0:
guard -= 1
autoplay._autoplay_battle(adr.runner(), battle, st, null)
check(adr.runner().current_battle() == null and adr.runner().awaiting_choice(),
"首战胜后选路态(非战斗期——current_battle 置空)")
check(adr.abandon(), "非战斗期 abandon 受理(选路态——同失败型结算)")
var sum: Dictionary = adr.summary
check(String(sum.get("result", "")) == "abandoned" and not bool(sum.get("cleared", true)),
"放弃结算:result=abandoned · cleared=false(已获面不追回——钥石累计 %d)" % int(sum.get("keystone_total", -1)))
check(not adr.abandon(), "结算后重复 abandon 拒绝(结算受理只算一次——state=idle)")
# ③ 冒险验收断言(映射 冒险结构.md 验收标准——表覆盖/满配通关/种子复现/大奖励/败北线/
# 重复拒绝/GP 跨关延续/加算区同账/恢复数学/放弃)
func _smoke_adventure() -> void:
print("--- 冒险结构验收(三关序列 · 大奖励 · 摘要五件事 · 账本封闭)---")
var autoplay = load("res://smoke/level_autoplay.gd")
# ① 表与覆盖义务(序列关数 = 行集关数;行集内每个战斗/Boss 节点跨表精确行——覆盖义务断言)
check(int(_db.level.rows.size()) == 3, "序列关数 = 行集关数(三关正式序列生效)")
var bg_keys := {}
for row in _db.board_gen.rows:
bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
var en_keys := {}
for row in _db.enemy.rows:
en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
var miss := 0
for row in _db.level.rows:
for n in row.nodes:
var nt := String(n.get("type", ""))
if nt != "battle" and nt != "boss":
continue
var key := "%d_%d" % [int(row.level_index), int(n.get("node_depth", -1))]
if not bg_keys.has(key) or not en_keys.has(key):
miss += 1
check(miss == 0, "行集内每个战斗/Boss 节点在 board_gen + enemy 有精确行(缺失 %d)" % miss)
# ② 满配通关 + ④ 大奖励兑付(恰 2 次 · 读数=表值 · 回满=新上限 · 覆盖值通道传播 L2/L3 首战上限)
var st: Dictionary = autoplay.run_adventure(_db, 42, 200, 100)
var sum: Dictionary = st["summary"]
check(String(st["result"]) == "completed" and bool(sum.get("cleared", false)),
"满配通关(42 / atk+200 / hp+100——cleared=true)")
check(int(sum.get("levels_cleared", -1)) == 3, "通关几关 = 3(levels_cleared)")
check(int(sum.get("keystone_total", -1)) == 3 * int(_db.adventure.keystone_per_level),
"钥石合计 = 3 × keystone_per_level(%d)" % int(sum.get("keystone_total", -1)))
check(int(sum.get("master_seed", -1)) == 42, "摘要携带主种子(42)")
var rewards: Array = st["reward_events"]
check(rewards.size() == 2, "大奖励恰兑付 2 次(L1/L2 后——末关不发上限提升)")
var hp_bonus: int = int(_db.adventure.level_clear_hp_bonus)
var reward_ok: bool = rewards.size() == 2
if reward_ok:
for i in rewards.size():
var r: Dictionary = rewards[i]
if int(r.get("hp_bonus", -1)) != hp_bonus or int(r.get("from_level", -1)) != i + 1 \
or int(r.get("new_max_hp", -1)) != int(_db.hero.max_hp) + hp_bonus * (i + 1) \
or int(r.get("keystone_gained", -1)) != int(_db.adventure.keystone_per_level):
reward_ok = false
check(reward_ok, "大奖励读数:hp_bonus=表值 · 新上限累加 · 回满=新上限 · 钥石逐关注账")
var seq: Array = st["encounter_max_hp_seq"]
var k := 2.0 # hp+100 ⇒ TunePanel 百分比后乘 ×2(加算先加、百分比后乘——生效次序口径)
var exp_base := maxi(1, int(round(float(int(_db.hero.max_hp)) * k)))
var exp_l2 := maxi(1, int(round(float(int(_db.hero.max_hp) + hp_bonus) * k)))
var exp_l3 := maxi(1, int(round(float(int(_db.hero.max_hp) + 2 * hp_bonus) * k)))
var idx_l2 := seq.find(exp_l2)
var idx_l3 := seq.find(exp_l3)
check(seq.size() > 0 and int(seq[0]) == exp_base,
"L1 首战上限入参 = 基础表值(%d——加算区冒险开始全 0)" % exp_base)
check(idx_l2 > 0 and idx_l3 > idx_l2,
"L2/L3 首战上限入参 = 基础+加算(%d→%d——大奖励经覆盖值通道传播)" % [exp_l2, exp_l3])
# ③ 种子域可复现(同种子两局:summary + 上限序列 + 行进序列逐位一致)
var st2: Dictionary = autoplay.run_adventure(_db, 42, 200, 100)
check(st2["summary"] == sum and st2["encounter_max_hp_seq"] == seq
and st2["visited_ids"] == st["visited_ids"],
"同种子两局逐位可复现(summary/上限序列/行进序列——主种子单点派生)")
# ⑤ 败北线(零配 L1 败北 + 半配 L2 败北——种子扫描,仿商店房 range(42,92) 先例)
var lose := {}
for s in range(42, 92):
var stl: Dictionary = autoplay.run_adventure(_db, s, 0, 0)
if String(stl["result"]) == "failed" and int(stl["summary"].get("stopped_at", -1)) == 1:
lose = {"s": s, "st": stl}
break
check(not lose.is_empty(), "零配 L1 败北种子可达(seed=%s)" % str(lose.get("s", -1)))
if not lose.is_empty():
var sl: Dictionary = lose["st"]["summary"]
check(not bool(sl["cleared"]) and int(sl["levels_cleared"]) == 0 \
and int(sl["keystone_total"]) == 0 and int(sl["stopped_at"]) == 1,
"L1 败北摘要:未打穿 · 通关 0 关 · 钥石 0 · 止步第 1 层")
var half := {}
for s in range(42, 142):
var sth: Dictionary = autoplay.run_adventure(_db, s, 50, 0)
if String(sth["result"]) == "failed" and int(sth["summary"].get("stopped_at", -1)) == 2:
half = {"s": s, "st": sth}
break
check(not half.is_empty(), "半配(atk+50)L2 败北种子可达(seed=%s)" % str(half.get("s", -1)))
if not half.is_empty():
var sh: Dictionary = half["st"]["summary"]
check(not bool(sh["cleared"]) and int(sh["levels_cleared"]) == 1 \
and int(sh["keystone_total"]) == int(_db.adventure.keystone_per_level) \
and int(sh["stopped_at"]) == 2,
"L2 败北摘要:L1 已过(钥石 1×表值)· 止步第 2 层")
# ⑥ 重复宣布拒绝(settle 后再调 _on_level_finished——摘要零变化)
var adr6 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(7))
adr6.set_hero_override(400, 200)
check(adr6.start_adventure(7), "重复拒绝场景:冒险启动(seed 7)")
var b6 = adr6.runner().current_battle()
var st6 := _adv_st()
var g6 := 30
while adr6.runner().current_battle() == b6 and g6 > 0:
g6 -= 1
autoplay._autoplay_battle(adr6.runner(), b6, st6, null)
check(adr6.abandon(), "重复拒绝场景就位:选路态放弃 → 已结算")
var sum6_before: Dictionary = adr6.summary.duplicate(true)
adr6._on_level_finished({"result": "victory", "hp": 10, "node_index": 1, "node_id": 1, "node_type": "battle"})
check(adr6.summary == sum6_before, "重复关卡结束宣布拒绝(账本与摘要零变化——契约 7)")
# ⑦ GP 跨关延续(L1 收束无 clear · 每战入账恰一次 · L2 商店房余额 > 0 · 冒险收束 clear 恰一次为末事件)
var wallet: Array = st["wallet_events"]
var clears := 0
var gains := 0
for ev in wallet:
if String(ev["event"]) == "clear":
clears += 1
elif String(ev["event"]) == "gain":
gains += 1
check(gains == int(st["battles"]), "每战完成入账恰一次(gain %d = battles %d)" % [gains, int(st["battles"])])
check(clears == 1 and wallet.size() > 0 and String(wallet[wallet.size() - 1]["event"]) == "clear",
"钱包 clear 恰一次且为末事件(关卡收束不清——GP 冒险收束清零归冒险结构)")
var shop_seq: Array = st["shop_gp_seq"]
check(shop_seq.size() == 2 and int(shop_seq[0]) > 0,
"L2 商店房到达余额 > 0(%d——L1 积累跨关延续;两关商店房必经各 1)" % (shop_seq[0] if shop_seq.size() > 0 else -1))
# ⑧ 加算区同账(手工场景:L2 商店购买 → 大奖励直调——同一本账累加 + 回满新上限)
var same := {}
for s in range(42, 92):
var a8 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s))
a8.set_hero_override(400, 200)
if not a8.start_adventure(s):
continue
if _adv_drive_to_shop(a8, _adv_st(), autoplay):
same = {"a": a8, "s": s}
break
check(not same.is_empty(), "同账场景种子可达(seed=%s——L2 商店房就位)" % str(same.get("s", -1)))
if not same.is_empty():
var a8 = same["a"]
var r8 = a8.runner()
var gp0: int = int(r8.gp)
check(gp0 >= 4, "进店余额 ≥ hp_up L2 首价 4(%d)" % gp0)
check(r8.buy_product("hp_up"), "购买 hp_up 受理(出账 + 加算区)")
check(int(a8.bonus.get("max_hp", -1)) == int(_db.adventure.level_clear_hp_bonus) + 3,
"统一冒险加算区含商店值(max_hp = %d[L1 大奖励] + 3[商店]——同一本账前半)" % int(_db.adventure.level_clear_hp_bonus))
a8._on_level_finished({"result": "victory", "hp": int(r8.hero_hp), "node_index": r8.node_index,
"node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟大奖励(victory 载荷直调——L2 非末关)
check(int(a8.bonus.get("max_hp", -1)) == 2 * int(_db.adventure.level_clear_hp_bonus) + 3,
"大奖励与商店购买同账累加(max_hp = %d + 3 + %d)" % [int(_db.adventure.level_clear_hp_bonus), int(_db.adventure.level_clear_hp_bonus)])
check(int(a8.ledger["hero_hp"]) == int(_db.hero.max_hp) + int(a8.bonus["max_hp"]),
"血回满 = 新上限(%d/%d)" % [int(a8.ledger["hero_hp"]), int(_db.hero.max_hp) + int(a8.bonus["max_hp"])])
# ⑨ 恢复点数学(接口③封顶公式三读数 + 同式两落点一致)
var a9 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(99))
check(int(a9.apply_rest(30, 70, 100)) == 30 and int(a9.apply_rest(30, 95, 100)) == 5 \
and int(a9.apply_rest(30, 100, 100)) == 0,
"恢复点数学:floor(100×30%)=30 · 封顶缺口 5 · 满血回 0 合法")
var lr9 = load("res://core/level_runner.gd").new(_db, _fresh_rng(99))
check(int(lr9.rest_heal_amount(30, 70, 100)) == 30 and int(lr9.rest_heal_amount(30, 95, 100)) == 5,
"同一公式两个落点逐位一致(LevelRunner.rest_heal_amount 内置默认)")
# ⑩ 放弃(战斗中拒绝 · 非战斗期受理同失败型结算)
var a10 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(15))
a10.set_hero_override(400, 200)
check(a10.start_adventure(15), "放弃场景:冒险启动(seed 15——首战在飞)")
check(not a10.abandon(), "战斗中 abandon 拒绝(战斗域单事务在飞)")
var b10 = a10.runner().current_battle()
var st10 := _adv_st()
var g10 := 30
while a10.runner().current_battle() == b10 and g10 > 0:
g10 -= 1
autoplay._autoplay_battle(a10.runner(), b10, st10, null)
check(a10.runner().current_battle() == null and a10.abandon(), "非战斗期(选路态)abandon 受理")
var s10: Dictionary = a10.summary
check(String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)),
"放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)")
# 手工场景驱动:L1 全程 + L2 推进到商店房(_drive_level 复用 + 兑付确认/选路/大奖励确认手工编排)
func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
var guard := 24
while guard > 0 and String(st["result"]) == "":
guard -= 1
autoplay._drive_level(a.runner(), st, null, "normal", "min")
if String(st["result"]) != "":
return false
if a._pending_next_level:
a.continue_to_next_level()
continue
var r = a.runner()
if String(r.current_node().get("type", "")) == "shop":
return true
if r.awaiting_choice():
var ids: Array = r.next_candidate_ids()
ids.sort()
r.pick_next_node(int(ids[0]))
continue
var t := String(r.current_node().get("type", ""))
if t == "reward" or t == "rest":
r.continue_after_node()
return false
# 手工场景状态袋(_drive_level/_autoplay_battle 消费的最小字段族——run_adventure st 的同构子集)
func _adv_st() -> Dictionary:
return {"result": "", "hp_left": 0, "battles": 0, "rounds": 0, "hero_dmg": 0,
"rest_healed": 0, "hp_before_rest": -1, "defeat_node_index": -1, "defeat_node_type": "",
"boss_depth": -1, "hp_at_boss": -1, "rows": [], "timeout": false, "log_path": "",
"policy": "normal", "refreshes": 0, "mine_hits": 0, "visited_ids": [], "choices": 0}
# ---- 战斗屏棋盘布局·格边长公式回归(2026-09-19 真人试玩 bug⑥:L2 12×12 板下缘被裁约 1/6 格高——
# 旧公式 N×cell ≤ 可用高−12 未计 (N−1)×2px 网格间距,网格实高溢出被 clip_contents 裁掉;
# 修复 = 公式计入间距 + 垂直预留 24px〔上 12/下 12——底部不贴 D 区边框〕)----