merge: 元进度实现批落籍——MetaTable 四行族+MetaProgression 账本+跨局注入+营地覆盖层(台账 120)
This commit is contained in:
commit
a5576ecb4a
27 files changed
+1092
-55
No files matched your search
@@ -44,7 +44,7 @@
|
||||
| `关卡节点地图.md` | 单关内的节点图选路与推进 | 已定稿 |
|
||||
| `冒险结构.md` | 整局冒险的关卡序列、失败与大奖励 | 已定稿·自旧体系迁移 |
|
||||
| `难度曲线与Boss.md` | 难度曲线、期望值带与 Boss 设计 | 已定稿·自旧体系迁移 |
|
||||
| `元进度.md` | 跨局成长(旧体系未单独成文) | 范围草案·待设计 |
|
||||
| `元进度.md` | 跨局成长(旧体系未单独成文) | 已定稿·已实现(2026-09-21 实现批,数值 provisional 归设计批标定) |
|
||||
|
||||
### 表现与支撑(`systems/表现与支撑/`)
|
||||
|
||||
@@ -101,3 +101,4 @@
|
||||
| v0.5 | 2026-09-16 | 待拍板收编批:新建顶层文档 `06-待拍板清单.md`,把 systems/ 各系统文档「待拍板」章节的条目与三份文档的散落未定项统一收编;各系统文档「待拍板」节改为指路存根,其后新增条目直接登记清单 |
|
||||
| v0.6 | 2026-09-16 | 06 清单改版(用户拍板):「待拍板」术语全仓改「待办」,顶层文档更名 `06-待办清单.md`;本表 06 行同步,各系统文档存根节随批由「待拍板」改名「待办」(各文版本历史不另记存根改名) |
|
||||
| v0.7 | 2026-09-17 | 关卡节点地图定稿批:`关卡节点地图.md` 状态行改**已定稿**(v1.0——分支节点图形态重开〔用户拍板重开 05 清单路径分支后置裁定〕、四类节点封闭枚举、全节点确认选路、遭遇构成编排面三条、节点奖励=功能道具正式渠道;概要行"节点链"改"节点图";配套 05 清单移除路径分支条目、06 清单三悬案收口 + 两条新增、registry level_table 与术语回填) |
|
||||
| v0.8 | 2026-09-21 | 元进度实现批:`元进度.md` 状态列「范围草案·待设计」→「已定稿·已实现」(2026-09-21 实现批落地,数值 provisional 归设计批标定) |
|
||||
@@ -52,6 +52,12 @@ MVP 不加新状态;扩容走 status_effects.tres 加行 + registry 登记。
|
||||
|
||||
数值全部 provisional(骨架先行、数值后置)——商店房节点的机制归[关卡节点地图](systems/冒险循环/关卡节点地图.md),GP 钱包账本归[冒险结构](systems/冒险循环/冒险结构.md),调参归[难度曲线与Boss](systems/冒险循环/难度曲线与Boss.md)曲线调参批。扩商品或改价格结构走 shop.tres 加行 + registry 登记。
|
||||
|
||||
## 元进度
|
||||
|
||||
**一套元进度配置**(`data/tables/meta.tres`,现行行数读表为准):属性锻造商品三行(生命上限 / 基础攻击 / 护甲等级的 flat 档与几何定价参数)、Boss 钥石两档三行(首杀 / 重复杀)、道具工坊阈值六行、区间平移换算率三行(数值占位 0 = 未标定合法态)。
|
||||
|
||||
数值全部 provisional(骨架先行、数值后置)——机制归[元进度](systems/冒险循环/元进度.md),数值标定随元进度设计批配读数。扩柜台或改定价结构走 meta.tres 加行 + registry 登记。
|
||||
|
||||
## 暂不做 / 后续再说
|
||||
|
||||
垂直切片阶段明确砍掉或推迟的内容,写在这里防止回潮:
|
||||
@@ -61,7 +67,6 @@ MVP 不加新状态;扩容走 status_effects.tres 加行 + registry 登记。
|
||||
- 地形变异扩展内容——首批四类型已落地,反制道具与全局作用域排 Alpha(2026-09-20 用户拍板)
|
||||
- 道具/增益之间的组合协同(遗物式联动)
|
||||
- 战后自动回复——回复只有道具与恢复点两个来源
|
||||
- 元进度与跨局成长
|
||||
- 新敌种、新棋盘档位、更高密度、同深度多敌种混搭
|
||||
- 魔法道具清单之外的新件、状态表之外的新状态
|
||||
|
||||
@@ -79,3 +84,4 @@ MVP 不加新状态;扩容走 status_effects.tres 加行 + registry 登记。
|
||||
| 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 编排;测试直入口保留);「更多关卡」行同步;正文零历史叙述纪律下仅状态口径更新,结构框架零改动 |
|
||||
| v0.9 | 2026-09-21 | **元进度实现批(与切片代码 / registry / 元进度 / 02 / 05 同批)**:「暂不做」节移除「元进度与跨局成长」行——跨局成长首批落地(营地三柜台 / 钥石账本 / 跨局注入,数值 provisional 归元进度设计批标定);新增「一套元进度配置」小节(meta.tres 四行族读表纪律,数值 provisional 归设计批) |
|
||||
@@ -19,7 +19,7 @@
|
||||
- **多波战斗**:Peglin 对比报告(2026-09-12)裁下的后置项。单场多波是战斗编排级的改动,波间状态保留规则也要重新定义,排 Alpha 期评估。
|
||||
- **遗物 / 构筑协同**:同批裁下。先做协同设计再谈加密度,没有协同的纯堆量只会稀释每件道具的存在感。
|
||||
- **战后回复**:同批裁下。要做得绑定敌方攻击力下调,单独加回复会直接破坏现有数值预期。
|
||||
- **元进度(钥石解锁 / 跨局成长)**:排 Alpha。先把单局循环做扎实,跨局成长是之后的事。
|
||||
- **元进度(钥石解锁 / 跨局成长)**:排 Alpha,首批已落地(2026-09-21 元进度实现批——营地三柜台 / 钥石账本 / 跨局注入,数值 provisional 归设计批标定)。铺量与标定仍是之后的事。
|
||||
- **反雪球机制**:MVP 不做。等 Alpha 拿到真实测试数据,再决定要不要治滚雪球、怎么治。
|
||||
- **正式音频、教学引导、战斗动画演出、设置与本地化**:MVP 内的打磨项,一律排后。先验证核心循环,再谈好听好看。
|
||||
- **连刷 / 换图资金跨战斗携带**:现行规则是每场战斗清零,防滚雪球。是否解禁连刷策略,等实测数据说话。
|
||||
|
||||
+3
-11
@@ -261,7 +261,7 @@
|
||||
|
||||
**归属**:随[元进度](systems/冒险循环/元进度.md)定稿
|
||||
|
||||
**状态**:待办(2026-09-18 注记:接口启用形态已裁决——区间平移换算率启用、换算率表首版三行定稿,本条剩余面 = 首版三行的折算数值随模拟读数标定,护甲行强条目须按解锁后参数重测)
|
||||
**状态**:待办(2026-09-21 元进度实现批收窄)——**表结构已建**(`data/tables/meta.tres` conversion 行族三行,数值占位 0 = 未标定合法态);剩余面 = 折算数值随设计批模拟读数标定 + 护甲行强条目须按解锁后参数重测
|
||||
|
||||
|
||||
#### 三方向各自的解锁清单与定价
|
||||
@@ -270,15 +270,7 @@
|
||||
|
||||
**归属**:元进度设计批
|
||||
|
||||
**状态**:部分收口(2026-09-18 元进度定稿批,用户拍板)——落地形态定稿:**营地三柜台**(属性锻造:生命上限 / 基础攻击 / 护甲等级,钥石直购可重复升级;道具工坊:六件辅助道具,首版全走关卡通关次数达标渠道、钥石直购价随定价批后补、最终双渠道并存;奖励格图鉴:结构位现定、格子内容排 Alpha 随地图战斗资源差异化批);定价曲线原则 = **几何递增**(起步价与公比数字待设计批配读数)。清单明细铺量与定价数字继续待办(本条剩余面)
|
||||
|
||||
#### 解锁注入的实现口径
|
||||
|
||||
**现状**:冒险结构预留的「冒险开始参数面」如何承载各方向解锁,随设计批与对方联合修订。来源:元进度 待设计问题清单第 5 条
|
||||
|
||||
**归属**:元进度与[冒险结构](systems/冒险循环/冒险结构.md)联合修订
|
||||
|
||||
**状态**:部分收口(2026-09-18 元进度定稿批,用户拍板)——**口径已定**:沿用「冒险开始参数面」+ registry `hero_override_parameter_passing` 通道,开始冒险时算好 override 逐字段显式传(-1 = 表值兜底不变);字段清单随柜台扩——属性三档为数值覆盖,道具池 / 遭遇权重 / 奖励格解锁为集合操作(参数面登记**追加 / 替换**两种语义)。参数面的正式侧联合修订执行仍随批(本条剩余面);口径成文于[元进度](systems/冒险循环/元进度.md) v1.0
|
||||
**状态**:部分收口(2026-09-21 元进度实现批再收窄)——**三柜台结构与 provisional 定价已落地**(属性锻造三档血+3/攻+1/甲+1、base 10 公比 150;Boss 两档 L1 5/1·L2 8/2·L3 12/3;工坊六件阈值 1/1/2/2/3/3 按总通关次数——全部读 `data/tables/meta.tres`);剩余面 = 定价数字与 Boss 两档、换算率数值随设计批配读数标定
|
||||
|
||||
#### 解锁界面与钥石呈现的交互形态
|
||||
|
||||
@@ -286,7 +278,7 @@
|
||||
|
||||
**归属**:元进度与[HUD 框架](systems/表现与支撑/HUD框架.md)协作批共同定
|
||||
|
||||
**状态**:部分收口(2026-09-18 元进度定稿批,用户拍板)——**双入口框架已定**:主菜单营地独立入口(消费钥石、管理永久强化)+ 局内暂停层只读查看(余额与已解锁);钥石余额 HUD 常驻。交互与视觉实现随 HUD 框架协作批、以表现层样张过审为前置(本条剩余面);口径成文于[元进度](systems/冒险循环/元进度.md) v1.0
|
||||
**状态**:部分收口(2026-09-21 元进度实现批再收窄)——**功能界面已落地**(主菜单营地入口 + CampOverlay 三柜台 + A 区顶栏钥石余额常驻 + 结算屏入账明细,ui_kit 程序占位视觉);剩余面 = 视觉终稿(以表现层样张过审为前置)与局内暂停层只读入口(现行无暂停层,随 HUD 批)
|
||||
|
||||
## 表现与支撑
|
||||
|
||||
|
||||
@@ -157,3 +157,4 @@ MVP 不要求本系统的闭环([不做清单](../../05-不做清单.md)整体
|
||||
| v1.0 | 2026-09-18 | **元进度定稿批(用户拍板六件收口)**:定名「永久强化」(系统族名元进度保留,「天赋树」退役、「营地」为界面壳);营地三柜台(属性锻造三档含护甲等级 / 道具工坊六件首版通关次数渠道 / 奖励格图鉴结构位);Boss 重复获取 = 首杀全量 + 重复杀固定小额;定价几何递增原则;区间平移换算率启用(换算率表首版三行、护甲行强条目、数值读 data/tables);注入 = 冒险开始参数面 + 覆盖值通道(override 集合操作:追加 / 替换);解锁界面双入口框架。本文自范围草案升定稿 |
|
||||
| v1.1 | 2026-09-18 | **数值语言拍板批(与 01-核心循环 数值语言条款 / 难度曲线与Boss / registry / 06-待办清单 / 切片代码同批)**:营地三柜台节新增投值语言锁定条款——属性投值语言锁定 flat 整数加减,数值投值仍归设计批 |
|
||||
| v1.2 | 2026-09-20 | GP+商店房批注记(知会性质——与冒险结构 v0.5 同批):钥石节新增与 GP 的分工一行——钥石 = 跨局持久解锁、GP(「金币」)= 冒险内一次性属性提升、局内清零不入存档,双币生命周期切分成文、永不互兑。机制零改动 |
|
||||
| v1.3 | 2026-09-21 | **元进度实现批(与切片代码 / registry / 02 / 03 / 05 / 06 同批)**:本批 = 本文的实现批——MetaTable 四行族建表(`data/tables/meta.tres`,数值全 provisional 归设计批配读数);MetaProgression 账本服务(余额/几何定价/购买/工坊解锁/Boss 两档入账/注入计算,Core 层零 UI 零信号);注入通道落地(start_adventure params.hero_override → LevelRunner base_override,叠加序 = 表值+永久强化 flat+加算区 flat × 测试面板 pct);呈现面(营地覆盖层三柜台+主菜单入口+A 区顶栏钥石余额+结算屏入账明细,ui_kit 程序占位——视觉终稿归 HUD 协作批)。正文零改动(实现面不回流正文——正文零历史叙述纪律) |
|
||||
@@ -76,7 +76,7 @@ entities:
|
||||
path: user://save.json(单档 MVP——多存档槽位 P2 保留)
|
||||
format: JSON 单档(人可读可调试;字典键一律 String)
|
||||
version: SAVE_FORMAT_VERSION=1(registry schema_version 同源;高版本拒读 + 禁写盘 + 内存默认新档运行;低版本读档时链式迁移 register_migration,任一步失败按损坏兜底)
|
||||
domains: meta 域六件——keystone_balance(内容 owner=元进度)/ forge_levels 三档 max_hp·atk·armor_class(元进度)/ item_unlocks(元进度·道具工坊消费)/ level_clear_counts(元进度)/ boss_first_kill(元进度)/ recent_summaries cap 10 冒险摘要五件事副本(内容 owner=冒险结构)
|
||||
domains: meta 域六件——keystone_balance(内容 owner=元进度)/ forge_levels 三档 max_hp·atk·armor_class(元进度)/ item_unlocks(元进度·道具工坊消费)/ level_clear_counts(元进度)/ boss_first_kill(元进度)/ recent_summaries cap 10 冒险摘要五件事副本(内容 owner=冒险结构;元素附 boss_drops 副本——Boss 两档金额入账随元进度实现批 2026-09-21 接通,apply_adventure_summary 第二参)
|
||||
write_timing: 启动读盘;正式冒险收束(adventure_finished → apply_adventure_summary)写盘;营地每笔钥石变更后即时写(元进度实现批接线);单关测试入口与调试两层不写档
|
||||
atomic_write: tmp + rename 原子写(写一半断电不留烂档——钥石不丢判据承载)
|
||||
corruption_policy: 坏档改名 <path>.corrupt.bak(已存在覆盖)+ 默认新档继续 + 警告留痕(不静默丢档不 crash)
|
||||
@@ -491,7 +491,7 @@ entities:
|
||||
owner: design/gdd/systems/排雷循环/魔法道具.md
|
||||
attributes:
|
||||
table: AuxItemTable(数据配置 承载【数据配置×① 修订批登记】,键 = item_id;魔法道具 回填 owner,2026-09-14 定稿登记;定义表非进度成长表——Rule 4 豁免;生效前提 = 地图战斗资源×①…⑤/数据配置×①② 修订批兑现)
|
||||
slice_scope: 垂直切片期无道具内容(2026-09-17 用户拍板——空清单合法态):AuxItemTable 不随切片建表、六行不入掉落池(掉落频率归战斗奖励侧),F2 维度覆盖谓词挂起至表非空;本条设计登记与机制桥接(战斗结算 query_reward_identity 未开格恒可查)保留,内容随 魔法道具内容批(排垂直切片收尾后)回填
|
||||
slice_scope: 垂直切片期无道具内容(2026-09-17 用户拍板——空清单合法态):AuxItemTable 不随切片建表、六行不入掉落池(掉落频率归战斗奖励侧),F2 维度覆盖谓词挂起至表非空;本条设计登记与机制桥接(战斗结算 query_reward_identity 未开格恒可查)保留,内容随 魔法道具内容批(排垂直切片收尾后)回填。**解锁状态面例外**(元进度实现批 2026-09-21 注记):工坊六件的通关次数解锁渠道已落地(meta.tres workshop_items 行族 + SaveManager item_unlocks 域),局内效果仍归 Alpha 魔法道具整体重设计批——名称沿用本条词表
|
||||
fields:
|
||||
item_id: String 主键唯一——地图战斗资源 RewardTable.effect_id(aux 行)与 VariationTable.counter_item_ids 的引用键
|
||||
display_name: String 非空(HUD框架 呈现数据源,本地化归 设置与本地化——敌人系统/增益状态/地图战斗资源 同库惯例)
|
||||
@@ -596,7 +596,7 @@ entities:
|
||||
attributes:
|
||||
kind: 主角战斗入参覆盖值通道(begin_battle 显式参数传递——切片 E4 封装收口批实现,registry 登记缺口由 冒险结构 定稿批补登)
|
||||
contract: begin_battle 增 hero_hp / hero_atk / hero_max_hp 显式入参 → 敌人系统.spawn 增 atk_override / max_hp_override 参数;-1 = 表值兜底(不覆盖);hero 表运行期不可变(数据配置 单一事实源机制化——表写通道全删)
|
||||
consumers: HP 账本(冒险结构——跨战斗 HP 持久性 + 大奖励上限提升的传入面)/ 元进度加成(元进度 远期预留)/ 切片 TunePanel(调试通道)
|
||||
consumers: HP 账本(冒险结构——跨战斗 HP 持久性 + 大奖励上限提升的传入面)/ 元进度加成(元进度实现批 2026-09-21 接通——MetaProgression.hero_override() 现算 flat 三档,AdventureRunner.start_adventure params.hero_override 显式传入 → LevelRunner base_override 共享;叠加序 = 表值 + 永久强化 flat + 加算区 flat × TunePanel pct)/ 切片 TunePanel(调试通道)
|
||||
adr_status: ADR-0009 候选——正式侧 战斗结算/敌人系统 GDD 修订随 ADR-0009 批;冒险结构 GDD 规则 3/4 为 冒险结构 半边(HP 账本供值)的正式定义源
|
||||
referenced_by: [战斗结算, 敌人系统, 冒险结构, 元进度]
|
||||
|
||||
@@ -604,7 +604,7 @@ entities:
|
||||
type: schema_data
|
||||
owner: design/gdd/systems/冒险循环/元进度.md
|
||||
attributes:
|
||||
table: 永久强化换算率表(数据配置 承载;键结构 元进度定稿批 2026-09-18 登记,数值行读 `data/tables/` 随元进度设计批投值——本条只登记结构,零数值)
|
||||
table: 永久强化换算率表(数据配置 承载;键结构 元进度定稿批 2026-09-18 登记,数值行读 `data/tables/` 随元进度设计批投值——本条只登记结构,零数值。**已建表**:meta.tres conversion 行族三行落地,数值占位 0 = 未标定合法态,provisional 待设计批标定(护甲行强条目在案)——元进度实现批 2026-09-21 注记)
|
||||
kind: 区间平移换算率(2026-09-18 用户拍板启用)——永久强化对难度的影响不直接改难度判据,按统一换算率折算为通关率带平移量后套用(完美档 gate 与 normal 目标带判读口径不变,带随解锁等级平移)
|
||||
key: (conversion_row_id)——一行一柜台商品类型
|
||||
first_version_rows: 首版登记三个属性维度(表内即三行条目;用户拍板 2026-09-18,"维度"为拍板用语、"行"为表内同义术语):生命上限强化 / 基础攻击强化 / 护甲等级强化——后续商品上架(道具工坊钥石价、奖励格图鉴)逐条补登记,每次上架伴随一行换算登记(逐行归因纪律)
|
||||
@@ -612,6 +612,19 @@ entities:
|
||||
value_semantics: 每行折算值 = 每级强化对应的通关率带平移量(pp);营地定价从对应行换算率推(几何递增曲线,起步价与公比随设计批)
|
||||
related_save_domains: 钥石余额 / 永久强化等级表 / 关卡通关次数表 / Boss 首杀标记位(域级契约登记方式随 存档序列化「存档契约条目的登记方式」联合裁决)
|
||||
referenced_by: [元进度, 难度曲线与Boss, 数据配置, 存档序列化, 地图战斗资源]
|
||||
|
||||
- name: meta_table
|
||||
type: table
|
||||
owner: design/gdd/systems/冒险循环/元进度.md
|
||||
attributes:
|
||||
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 两档 L1 5/1 · L2 8/2 · L3 12/3;工坊阈值 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 未细分);钥石直购价随定价批后补
|
||||
referenced_by: [元进度, 存档序列化, 数据配置]
|
||||
|
||||
# 2026-09-09 奖励道具经济 full 评审修订批(owner=奖励道具经济 代用户修订,NEEDS REVISION 14 blocking 同日核销):
|
||||
# 新增 reward_config 条目(RewardConfigTable 单行全局——T_fog_min/K_dmg/N_draft/P_min 的 Rule 1 载体);
|
||||
# reward_table 增 display_name(九字段)+ 默认根行集登记 + direction_precheck(刀供给)+ 跨字段护栏两条
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||
|
||||
模拟日志: user://logs/playtest_4200_20260921T042329.jsonl
|
||||
=== 关卡期望扫描:level 1 · N=20/格 · 种子 4200+20 · 策略=normal · 种子模式=shared ===
|
||||
策略 atk%/hp% 通关率 整关承伤均/p90 通关剩余HP均(胜) 进BossHP均 平均轮数 踩雷/战 普通承伤/场% 跳战率 Boss承伤% 踩雷/局 刷新/局
|
||||
基线 atk +0% hp +0% 35% 16.4 / 22.0 4.3 14.0 15.2 0.03 14.6 7% 44.9 0.15 0.00 6.boss×12 4.battle×1
|
||||
(F4 带:承伤 ≤20%/普通场 · ≤50%/Boss 场,占生效 max_hp · F5 带:基线通关率 L1 [35,55]% / L2 [25,45]%——2026-09-20 用户拍板分带 · 跳战率 = 普通战斗 round-1 即胜(首轮全灭)占比 · 承伤含败者至死 · 败北节点 0-3=战斗节点序 4=Boss · 踩雷/战=每战斗负向兑付触发期望(格面资源批口径) · 完美档 = 技能轴上界测量(零盲挖/全标旗/卡死刷新——口径见 auto_player.gd)· 耗时 21.2 秒)
|
||||
WARNING: 49 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||
at: cleanup (core/object/object.cpp:2536)
|
||||
ERROR: 26 resources still in use at exit (run with --verbose for details).
|
||||
at: clear (core/io/resource.cpp:822)
|
||||
@@ -19,7 +19,7 @@
|
||||
| 2026-09-18 | 加法核数值语言拍板落地(台账 75)+ 80 遍模拟试玩基线(台账 76);无伤通关根因批(行动点压速 8→6);英雄双轴批(甲/攻类型可配置);伤害道具批五型→七型;战斗数值盘问会话四收口;关卡节点地图 v1.0 定稿(分支节点图拍板);元进度定名永久强化+营地三柜台 |
|
||||
| 2026-09-19 | 验收读数重测批落地(4708940);掷点流种子偏置根因修复 A+B 案(R3 混淆派生 + MC unique 模式);插旗不可逆批(台账 87,冒烟 331);普通画像 v2 基础定式拍板(普通=基础定式/完美=上界仪);技能池新效应词汇批(妨害技+必中附加分量,smoke 306);清图支配性对账重跑(c8e5afd);迭代闭环仪表盘落地(台账 78);worktree 铁律立规 |
|
||||
| 2026-09-20 | 第三关内容批(灰矮人矿城行集 + 测试直入口,smoke 375 全绿);GP+商店房实施批(九型格面+随机掉落+Boss前商店房+统一加算区,smoke 437 全绿);八型批呈现层漏网修复(真人试玩卡断——行协议呈现层同步闸 11 断言);第一层定名「地精洞窟」 |
|
||||
| 2026-09-21 | **冒险结构实现批:切片从三个单关 demo 升级为一局完整三关冒险**(AdventureRunner+LevelRunner 账本外置+AdventureTable 建表;三关正式序列生效;smoke 482 全绿);存档序列化批(SaveManager 落地:单档 JSON 存档读写/版本迁移/损坏兜底 + 元进度四件与冒险摘要入档,smoke 518 全绿);放弃入口界面呈现批(用户拍板「放弃接口也要有」——选路停点挂放弃按钮,台账 119)——元进度批(钥石消费)挂点就位 |
|
||||
| 2026-09-21 | **冒险结构实现批:切片从三个单关 demo 升级为一局完整三关冒险**(AdventureRunner+LevelRunner 账本外置+AdventureTable 建表;三关正式序列生效;smoke 482 全绿);存档序列化批(SaveManager 落地:单档 JSON 存档读写/版本迁移/损坏兜底 + 元进度四件与冒险摘要入档,smoke 518 全绿);放弃入口界面呈现批(用户拍板「放弃接口也要有」——选路停点挂放弃按钮,台账 119);元进度实现批(MetaTable 四行族+MetaProgression 账本+跨局注入+营地覆盖层与结算明细,smoke 549 全绿——跨局循环闭合,数值 provisional 归设计批标定) |
|
||||
|
||||
## 路线图(主线路线与门)
|
||||
|
||||
|
||||
@@ -530,3 +530,13 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
|
||||
**冒险级语义**(设计权威 = 冒险结构.md v0.6):主种子恰一次注入(冒险内不重播种,同种子同操作序列逐位可复现);生命账本跨关同账(战后回写、恢复点按比例封顶、大奖励回满);GP 钱包跨关延续、冒险收束清零;统一冒险加算区(商店购买与大奖励同账);非末关通关发大奖励(上限提升 +10 读 adventure.tres、血回满、钥石 +10),末关不发上限提升、钥石照记;任意战斗倒下 = 冒险失败;结算屏出冒险摘要五件事(打穿没有/通关几关/止步第几关/钥石合计/主种子)。
|
||||
|
||||
**验证**:冒烟 437→482 项 0 失败三闸全过(冒险表 +4 / API 闸 +8 / 冒险验收 +33:表覆盖义务、满配通关、种子复现、大奖励恰 2 次与覆盖值传播、败北线两种子扫描、重复宣布拒绝、GP 跨关延续、加算区同账、恢复数学、放弃两态);MC 回归零行为变更(run_level 旧路径同种子逐位一致)。
|
||||
|
||||
## 2026-09-21 元进度实现批(跨局循环闭合——MetaTable + MetaProgression + 营地 + 跨局注入)
|
||||
|
||||
**跨局成长首批落地**(设计权威 = 元进度.md v1.2):「存档在变厚」成为玩家可感的现实——冒险收束入账(大奖励渠道 + Boss 首杀/重复杀两档,明细在结算屏可归因)、营地消费、余额 HUD 常驻、永久强化在下一局开局注入。
|
||||
|
||||
**玩法面**:首页新增「营地 · 永久强化」入口(三柜台:属性锻造三档 flat 直购可重复升级、几何递增定价;道具工坊六件按累计通关次数解锁——局内效果排 Alpha 魔法道具重设计批;奖励格图鉴占位锁定);A 区顶栏钥石余额常驻;冒险结算屏追加钥石入账明细(大奖励 / 逐关 Boss 首杀或重复 / 工坊新解锁)。买强化 → 下一局开局即生效(表值 + 永久强化 flat + 冒险加算区,再乘调整板百分比)。
|
||||
|
||||
**技术面**:`data/tables/meta.tres` 四行族(forge_products / boss_keystone / workshop_items / conversion)+ `core/meta_progression.gd` 账本服务(Core 层零 UI 零信号);SaveManager.apply_adventure_summary 增第二参 boss_drops(缺省 [] 零行为兼容,摘要元素附副本);AdventureRunner.start_adventure 增 params.hero_override(缺省全零 = 白板)→ LevelRunner base_override。**数值全 provisional 归元进度设计批配读数**(骨架先行先例 = GP+商店房批)。
|
||||
|
||||
**验证**:冒烟 522→549 项 0 失败三闸全过(表结构 +4 / 定价与购买 +7 / 注入与白板 +8 / 入账工坊摘要 +8);四探针全 PASS(ui_probe 营地开合/禁用态、click_probe 营地购买合成点击全路径——隔离存档换注保确定性,禁碰真实档);白板成立 = 空参数 = 零强化参数 = 纯表值三轴逐位一致。
|
||||
@@ -55,6 +55,7 @@ var summary := {} # 最近一次结算的冒险摘要(
|
||||
var _runner: LevelRunner # 当前关卡行进器(每关新建——关卡域状态天然隔离)
|
||||
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
|
||||
var _override := [0, 0] # TunePanel 百分比通道 [atk_pct, hp_pct](每关新 runner 都要应用)
|
||||
var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板)
|
||||
|
||||
|
||||
func _init(db_ref, rng_ref) -> void:
|
||||
@@ -65,8 +66,10 @@ func _init(db_ref, rng_ref) -> void:
|
||||
# ---- 冒险生命周期 ----
|
||||
|
||||
# 冒险开始(主种子注入恰一次——全冒险唯一 rng.new_adventure 调用点;重开 = 全新一轮:
|
||||
# 账本/加算区/已购档/钥石/摘要全重置,生命置满基础上限,从第 1 关起步)
|
||||
func start_adventure(seed_val: int) -> bool:
|
||||
# 账本/加算区/已购档/钥石/摘要全重置,生命置满基础上限,从第 1 关起步)。
|
||||
# params.hero_override = 永久强化 flat 注入(元进度实现批——缺省 {} = 全零白板,与 B1 空参数路径逐位一致;
|
||||
# -1/缺键 = 表值兜底口径,规范化为 0 flat)
|
||||
func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
if state != "idle":
|
||||
push_error("[冒险结构] start_adventure 拒绝——state=%s(重开须待上一局结算完毕)" % state)
|
||||
return false
|
||||
@@ -80,11 +83,20 @@ func start_adventure(seed_val: int) -> bool:
|
||||
levels_total = db.level.rows.size()
|
||||
level_index = 1
|
||||
_pending_next_level = false
|
||||
ledger["hero_hp"] = int(db.hero.max_hp) # 生命账本置满(基础上限读英雄表——加算区刚归零)
|
||||
_base_override = _normalize_override(params.get("hero_override", {}))
|
||||
ledger["hero_hp"] = int(db.hero.max_hp) + int(_base_override["max_hp"]) # 生命账本置满(表值+永久强化 flat——加算区刚归零)
|
||||
state = "running"
|
||||
return _start_current_level()
|
||||
|
||||
|
||||
# 注入参数规范化:三字段缺键/-1 = 表值兜底(→ 0 flat);负值一律 clamp 0(白板 = 全零合法态)
|
||||
func _normalize_override(raw: Dictionary) -> Dictionary:
|
||||
var out := {"atk": 0, "max_hp": 0, "armor_class": 0}
|
||||
for k in out.keys():
|
||||
out[k] = maxi(0, int(raw.get(k, 0)))
|
||||
return out
|
||||
|
||||
|
||||
# TunePanel 百分比通道(测试面板覆盖——每关新 runner 都要应用;正式侧归元进度)
|
||||
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
||||
_override = [atk_pct, hp_pct]
|
||||
@@ -139,9 +151,10 @@ func _start_current_level() -> bool:
|
||||
_runner.wallet_changed.connect(_forward_wallet_changed)
|
||||
_runner.level_finished.connect(_on_level_finished)
|
||||
_runner.set_hero_override(int(_override[0]), int(_override[1]))
|
||||
# 接口①开始关卡:生命经共享账本交接(账本当前值即开局生命——冒险开始置满 / 大奖励回满已写好)
|
||||
# 接口①开始关卡:生命经共享账本交接(账本当前值即开局生命——冒险开始置满 / 大奖励回满已写好);
|
||||
# _base_override 共享引用随载荷传入(永久强化 flat——LevelRunner.effective_* 叠加序第二项)
|
||||
var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased,
|
||||
Callable(self, "apply_rest"))
|
||||
Callable(self, "apply_rest"), _base_override)
|
||||
if ok:
|
||||
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
|
||||
return ok
|
||||
@@ -164,14 +177,14 @@ func _on_level_finished(payload: Dictionary) -> void:
|
||||
# 大奖励(非末关通关恰一次):上限提升写加算区 → 血回满 = 新上限 → 钥石入账(顺序即契约)
|
||||
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"])
|
||||
ledger["hero_hp"] = int(db.hero.max_hp) + int(_base_override["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"]),
|
||||
"new_max_hp": int(db.hero.max_hp) + int(_base_override["max_hp"]) + int(bonus["max_hp"]),
|
||||
"keystone_gained": int(db.adventure.keystone_per_level),
|
||||
"keystone_total": keystone,
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ const TABLE_MANIFEST := {
|
||||
"level": "level.tres",
|
||||
"difficulty": "difficulty.tres",
|
||||
"shop": "shop.tres", # GP+商店房实施批 2026-09-21(表与 schema 落籍归并行数据面——本批只接线加载与消费)
|
||||
"meta": "meta.tres", # 元进度实现批 2026-09-21(MetaTable 四行族——schema 与表同批落籍)
|
||||
"adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点)
|
||||
}
|
||||
|
||||
@@ -38,6 +39,8 @@ var difficulty: DifficultyTable
|
||||
# GP+商店房实施批(2026-09-21):ShopTable 暂持无类型注记——schema class_name 落籍归并行数据面,
|
||||
# 落籍后可收紧为 `var shop: ShopTable`(消费面一律走下方 shop_* 访问器,禁直读行集)
|
||||
var shop = null
|
||||
# 元进度实现批(2026-09-21):MetaTable 类型化持用(schema 与表同批落籍,无 shop 先例的落籍间隙问题)
|
||||
var meta: MetaTable
|
||||
var adventure: AdventureTable
|
||||
|
||||
var is_ready := false
|
||||
@@ -121,6 +124,7 @@ func load_tables(root: String, collect_only: bool) -> bool:
|
||||
level = loaded.get("level")
|
||||
difficulty = loaded.get("difficulty")
|
||||
shop = loaded.get("shop")
|
||||
meta = loaded.get("meta")
|
||||
adventure = loaded.get("adventure")
|
||||
is_ready = not has_errors
|
||||
return not has_errors
|
||||
@@ -378,6 +382,46 @@ func _shop_rows(family: String) -> Array:
|
||||
return arr if arr is Array else []
|
||||
|
||||
|
||||
# ---- 元进度实现批(2026-09-21):MetaTable 消费面(行集解析单点——MetaProgression/呈现层禁自扫表)----
|
||||
# 行族四轴(数据契约,行集归 meta.tres,数值全 provisional 归元进度设计批配读数):
|
||||
# forge_products {attr, display_name, flat_value, base_price, price_ratio_pct} ·
|
||||
# boss_keystone {level_index, first_kill, repeat_kill} ·
|
||||
# workshop_items {item_id, display_name, clear_threshold} ·
|
||||
# conversion {attr, per_level_shift_pct}。
|
||||
|
||||
func meta_forge_row(attr: String) -> Dictionary:
|
||||
for row in _meta_rows("forge_products"):
|
||||
if String(row.get("attr", "")) == attr:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
func meta_boss_keystone_row(level_index: int) -> Dictionary:
|
||||
for row in _meta_rows("boss_keystone"):
|
||||
if int(row.get("level_index", -1)) == level_index:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
func meta_workshop_items() -> Array:
|
||||
return _meta_rows("workshop_items")
|
||||
|
||||
|
||||
func meta_conversion_row(attr: String) -> Dictionary:
|
||||
for row in _meta_rows("conversion"):
|
||||
if String(row.get("attr", "")) == attr:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# 行族数组读取(MetaTable 属性名 = 行族名;表缺失/属性缺失 = 空集非错误态)
|
||||
func _meta_rows(family: String) -> Array:
|
||||
if meta == null:
|
||||
return []
|
||||
var arr = meta.get(family)
|
||||
return arr if arr is Array else []
|
||||
|
||||
|
||||
# 技能行单点读取(敌人技能池批——单位容器执行上报 / 战斗结算兑现的消费面)
|
||||
func skill_row(skill_id: String) -> Dictionary:
|
||||
return skill.row_by_id(skill_id)
|
||||
|
||||
@@ -58,6 +58,7 @@ var gp: int: # GP 冒险钱包(局内货币—
|
||||
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 base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 注入(元进度实现批——冒险模式 = AdventureRunner 共享字典经 start_level_external 注入;单关测试模式 = 私有全零)
|
||||
var _shop_purchased := {} # product_id → 已购档数(现价重算输入;冒险开始清零)
|
||||
# 协作对象私有(E1/E3 封装收口 2026-09-13):settlement 读取经 current_battle() 成文出口
|
||||
var _settlement = null # 当前战斗编排器(关卡结束置 null)
|
||||
@@ -84,21 +85,21 @@ func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
||||
hero_hp_pct = hp_pct
|
||||
|
||||
|
||||
# 叠加次序(GP+商店房实施批):基础表值 + 加算区(商店/大奖励 flat 先加)→ TunePanel 百分比后乘
|
||||
# (调整板 = 测试工具按现状保持——百分比恒乘在加算生效值上)
|
||||
# 叠加次序(元进度实现批钉死):表值 → 永久强化 flat(base_override)→ 加算区 flat(bonus)先加
|
||||
# → TunePanel 百分比后乘(调整板 = 测试工具恒乘最后——代拍板 6,与 GP 批既有次序兼容)
|
||||
func effective_atk() -> int:
|
||||
var base: int = int(db.hero.atk) + int(bonus.get("atk", 0))
|
||||
var base: int = int(db.hero.atk) + int(base_override.get("atk", 0)) + int(bonus.get("atk", 0))
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(hero_atk_pct)) / 100.0)))
|
||||
|
||||
|
||||
func effective_max_hp() -> int:
|
||||
var base: int = int(db.hero.max_hp) + int(bonus.get("max_hp", 0))
|
||||
var base: int = int(db.hero.max_hp) + int(base_override.get("max_hp", 0)) + int(bonus.get("max_hp", 0))
|
||||
return maxi(1, int(round(float(base) * (100.0 + float(hero_hp_pct)) / 100.0)))
|
||||
|
||||
|
||||
# 护甲等级生效值(加算区 armor_class 轴——无 TunePanel 百分比通道,纯 flat)
|
||||
# 护甲等级生效值(永久强化 flat + 加算区 armor_class 轴——无 TunePanel 百分比通道,纯 flat)
|
||||
func effective_ac() -> int:
|
||||
return maxi(0, int(db.hero.armor_class) + int(bonus.get("armor_class", 0)))
|
||||
return maxi(0, int(db.hero.armor_class) + int(base_override.get("armor_class", 0)) + int(bonus.get("armor_class", 0)))
|
||||
|
||||
|
||||
# 覆盖复位(关卡结束归零——表值不可变,仅清百分比;调用方兼容保留)
|
||||
@@ -128,6 +129,7 @@ func start_level(level_index: int, seed_val: int) -> bool:
|
||||
_graph_mode = false
|
||||
gp = 0 # 冒险钱包清零开局(GP+商店房实施批——局内货币生命周期起点)
|
||||
bonus = {"atk": 0, "max_hp": 0, "armor_class": 0} # 加算区冒险开始全 0
|
||||
base_override = {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 单关测试模式恒全零(白板——单关入口零元进度)
|
||||
_shop_purchased = {} # 已购档数清零(跨冒险不延续——递增定价按冒险内累计)
|
||||
for node in nodes():
|
||||
if node.has("next"):
|
||||
@@ -146,11 +148,12 @@ func start_level(level_index: int, seed_val: int) -> bool:
|
||||
|
||||
# 冒险结构接口①:开始关卡(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接;
|
||||
# 种子域归冒险结构:本路径不重播种;账本归冒险结构:本路径不重置 gp/bonus/_shop_purchased,
|
||||
# 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在 ledger 写好)
|
||||
# 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在 ledger 写好)。
|
||||
# base_override = 永久强化 flat 共享引用(元进度实现批——缺省 {} 全零白板,与 B1 空参数路径逐位一致)。
|
||||
# 流程与 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:
|
||||
purchased_ref: Dictionary, rest_handler: Callable, base_override_ref: Dictionary = {}) -> bool:
|
||||
var row := _find_level_row(level_index)
|
||||
if row.is_empty():
|
||||
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
|
||||
@@ -158,6 +161,7 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti
|
||||
_external = true
|
||||
_ledger = ledger
|
||||
bonus = bonus_ref
|
||||
base_override = base_override_ref
|
||||
_shop_purchased = purchased_ref
|
||||
_rest_handler = rest_handler
|
||||
level_row = row
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 钥石能否在营地换成永久强化并在下一局开局生效——冒险收束入账(大奖励 + Boss
|
||||
# 首杀/重复杀两档)、营地消费、注入计算的跨局账本能否在零 UI 下独立成立(元进度 GDD 验收标准)
|
||||
# Date: 2026-09-21(元进度实现批——设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2)
|
||||
# 元进度账本服务(Core 层零 UI、纯被动服务——同 StatusContainer 先例:零信号零外发,读存档算账)。
|
||||
# 职责三件:① 属性锻造定价与购买(几何递增——tier 级价格 = 迭代 floor(p × ratio/100),tier 0 =
|
||||
# base_price,floor 单一取整对齐 round_direction 纪律);② 道具工坊解锁(首版只挂总通关次数渠道——
|
||||
# Σ level_clear_counts 达标,代拍板 2;六件局内效果归 Alpha 魔法道具重设计批,本类只有解锁状态);
|
||||
# ③ 冒险收束入账(Boss 两档 = 表值 first_kill/repeat_kill,首杀标记读存档判first;调 SaveManager
|
||||
# apply_adventure_summary 入账并附 boss_drops 副本——复盘可归因,验收 5)。
|
||||
# 注入面:hero_override() = 三档 forge flat(全零 = 白板合法态;override 不写表——表运行期只读,
|
||||
# 消费方 AdventureRunner/LevelRunner 经冒险开始参数面显式传参)。
|
||||
# 纪律:数值零字面量——价格/档位/阈值/两档金额只读 db.meta_* 访问器;存档只经 SaveManager 访问器,
|
||||
# 禁读内部结构;行集解析禁自扫表(一律走 ConfigDB 访问器)。
|
||||
class_name MetaProgression
|
||||
extends RefCounted
|
||||
|
||||
var db
|
||||
var save
|
||||
|
||||
|
||||
func _init(db_ref, save_ref) -> void:
|
||||
db = db_ref
|
||||
save = save_ref
|
||||
|
||||
|
||||
func keystone_balance() -> int:
|
||||
return save.keystone_balance()
|
||||
|
||||
|
||||
# ---- 属性锻造(钥石直购、可重复升级——几何递增定价)----
|
||||
|
||||
func forge_level(attr: String) -> int:
|
||||
return save.forge_level(attr)
|
||||
|
||||
|
||||
# 每级强化 flat 值 × 当前级(注入面的算料——投值语言锁定 flat 整数)
|
||||
func forge_flat(attr: String) -> int:
|
||||
return forge_level(attr) * int(db.meta_forge_row(attr).get("flat_value", 0))
|
||||
|
||||
|
||||
# 下一级价格:tier = 当前级,迭代 floor(p × ratio/100) 恰 tier 次(tier 0 = base_price)
|
||||
func forge_price(attr: String) -> int:
|
||||
var row: Dictionary = db.meta_forge_row(attr)
|
||||
if row.is_empty():
|
||||
return 0
|
||||
var p := int(row.get("base_price", 0))
|
||||
var ratio := int(row.get("price_ratio_pct", 100))
|
||||
for _i in forge_level(attr):
|
||||
p = int(floor(float(p) * float(ratio) / 100.0))
|
||||
return p
|
||||
|
||||
|
||||
# 购买:余额不足 / attr 出域 false 零副作用;成功 = 消费 + 升级 + 写盘
|
||||
func buy_forge(attr: String) -> bool:
|
||||
if db.meta_forge_row(attr).is_empty():
|
||||
return false
|
||||
if not save.spend_keystone(forge_price(attr)):
|
||||
return false
|
||||
if not save.set_forge_level(attr, forge_level(attr) + 1):
|
||||
return false
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# ---- 道具工坊(首版全走通关次数渠道——钥石直购价随定价批后补,最终双渠道并存)----
|
||||
|
||||
# 总通关次数 = Σ level_clear_counts(工坊解锁判定量——代拍板 2)
|
||||
func _total_clears() -> int:
|
||||
var total := 0
|
||||
for i in db.level.rows.size():
|
||||
total += save.clear_count(i + 1)
|
||||
return total
|
||||
|
||||
|
||||
# 工坊六行只读投影(呈现层数据源):{item_id, display_name, threshold, unlocked, progress}
|
||||
func workshop_status() -> Array:
|
||||
var total := _total_clears()
|
||||
var out: Array = []
|
||||
for row in db.meta_workshop_items():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
out.append({
|
||||
"item_id": iid,
|
||||
"display_name": String(row.get("display_name", "")),
|
||||
"threshold": int(row.get("clear_threshold", 0)),
|
||||
"unlocked": save.item_unlocked(iid),
|
||||
"progress": total,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
# 达标未解锁 → unlock_item + 写盘;返回新解锁 item_id 列(冒险收束后与营地打开时各调一次)
|
||||
func refresh_workshop_unlocks() -> Array:
|
||||
var new_unlocks: Array = []
|
||||
var total := _total_clears()
|
||||
for row in db.meta_workshop_items():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if save.item_unlocked(iid):
|
||||
continue
|
||||
if total >= int(row.get("clear_threshold", 99)):
|
||||
save.unlock_item(iid)
|
||||
new_unlocks.append(iid)
|
||||
if not new_unlocks.is_empty():
|
||||
save.write_to_disk()
|
||||
return new_unlocks
|
||||
|
||||
|
||||
# ---- Boss 两档与收束入账(结算屏文案数据源)----
|
||||
# 明细 {big_reward, boss_drops: [{level, first, amount}], total, new_unlocks};
|
||||
# 入账与标记经 save.apply_adventure_summary(boss_drops 副本随摘要入 recent_summaries)。
|
||||
func settle_adventure(summary: Dictionary) -> Dictionary:
|
||||
var drops: Array = []
|
||||
for li in range(1, int(summary.get("levels_cleared", 0)) + 1):
|
||||
var first: bool = not save.boss_first_kill_marked(li)
|
||||
var row: Dictionary = db.meta_boss_keystone_row(li)
|
||||
var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0))
|
||||
drops.append({"level": li, "first": first, "amount": amount})
|
||||
save.apply_adventure_summary(summary, drops)
|
||||
var new_unlocks := refresh_workshop_unlocks()
|
||||
save.write_to_disk()
|
||||
var total := int(summary.get("keystone_total", 0))
|
||||
for d in drops:
|
||||
total += int(d["amount"])
|
||||
return {
|
||||
"big_reward": int(summary.get("keystone_total", 0)),
|
||||
"boss_drops": drops,
|
||||
"total": total,
|
||||
"new_unlocks": new_unlocks,
|
||||
}
|
||||
|
||||
|
||||
# ---- 跨局注入(冒险开始参数面——字段清单随柜台扩展,现 = 属性锻造三档)----
|
||||
# 全零 = 白板合法态(无解锁不得改变任何局内规则);override 禁写表(表运行期只读)。
|
||||
func hero_override() -> Dictionary:
|
||||
return {
|
||||
"atk": forge_flat("atk"),
|
||||
"max_hp": forge_flat("max_hp"),
|
||||
"armor_class": forge_flat("armor_class"),
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b22wboiq6n3ra
|
||||
@@ -171,17 +171,23 @@ func recent_summaries() -> Array:
|
||||
|
||||
|
||||
# 域更新原语(正式冒险收束唯一入账口——组合根经本函数入账,禁直改内部结构):
|
||||
# 钥石入账 + 逐通关关次 clear_count +1 与 Boss 首杀标记 + 摘要头部插入(cap 10,最新在前)。
|
||||
# 本批不含 Boss 金额(首杀全量/重复杀小额归元进度批——届时增第二参 boss_drops,默认 [] 零行为兼容)。
|
||||
func apply_adventure_summary(summary: Dictionary) -> void:
|
||||
# 钥石入账(summary.keystone_total + boss_drops 逐笔 amount)+ 逐通关关次 clear_count +1 与
|
||||
# Boss 首杀标记 + 摘要头部插入(cap 10,最新在前,元素附 boss_drops 副本——复盘可归因)。
|
||||
# boss_drops 归元进度批消费(两档金额读 meta 表——MetaProgression.settle_adventure 组装;
|
||||
# 缺省 [] 零行为兼容——调用方不带两档时入账面与批前逐位一致)。
|
||||
func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> void:
|
||||
add_keystone(int(summary.get("keystone_total", 0)))
|
||||
for drop in boss_drops:
|
||||
add_keystone(int(drop.get("amount", 0)))
|
||||
var cleared := int(summary.get("levels_cleared", 0))
|
||||
for li in range(1, cleared + 1):
|
||||
var key := str(li)
|
||||
_data["meta"]["level_clear_counts"][key] = int(_data["meta"]["level_clear_counts"].get(key, 0)) + 1
|
||||
_data["meta"]["boss_first_kill"][key] = true
|
||||
var entry: Dictionary = summary.duplicate(true)
|
||||
entry["boss_drops"] = boss_drops.duplicate(true)
|
||||
var arr: Array = _data["meta"]["recent_summaries"]
|
||||
arr.push_front(summary.duplicate(true))
|
||||
arr.push_front(entry)
|
||||
while arr.size() > SUMMARY_CAP:
|
||||
arr.pop_back()
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否在营地用钥石换永久强化、且强化在下一局开局生效——存档变厚成为玩家可感的现实
|
||||
#(元进度 GDD 验收标准,本批 = 其实现批)
|
||||
# Date: 2026-09-21(元进度实现批——设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2)
|
||||
# 数据配置 MetaTable schema(元进度/永久强化 行族四轴)。
|
||||
# 行族四轴(数据契约对齐 ConfigDB.meta_* 访问器——消费面一律走访问器,禁直读行集):
|
||||
# forge_products = 属性锻造商品行(键 attr:{max_hp, atk, armor_class} 恰三唯一——注入契约三字段必齐;
|
||||
# 投值语言锁定 flat 整数;几何定价 tier 级价格 = 迭代 p = floor(p × price_ratio_pct/100),tier 0 = base_price)。
|
||||
# boss_keystone = Boss 钥石两档行(level_index 连续从 1;first_kill ≥ repeat_kill——首杀全量、
|
||||
# 重复杀固定小额恒定不递减,两档数值归元进度设计批配读数)。
|
||||
# workshop_items = 道具工坊行(item_id = registry aux_items 六件词表封闭恰全;首版解锁渠道 = 总通关次数
|
||||
# 达标 clear_threshold——2026-09-20 排期裁决;局内效果整体归 Alpha 魔法道具重设计批,本表只有解锁状态面)。
|
||||
# conversion = 区间平移换算率行(attr 与 forge 词表一致恰三;per_level_shift_pct ∈ [0,100]——
|
||||
# registry 键 meta_progression_conversion_table;护甲行强条目:验收读数必须按解锁后参数重测)。
|
||||
# 行值全部 provisional 归 元进度设计批配读数(用户拍板骨架先行,先例 = GP+商店房批)。
|
||||
class_name MetaTable
|
||||
extends Resource
|
||||
|
||||
@export var forge_products: Array = [] # Array[Dictionary] 键 attr
|
||||
@export var boss_keystone: Array = [] # Array[Dictionary] 键 level_index
|
||||
@export var workshop_items: Array = [] # Array[Dictionary] 键 item_id
|
||||
@export var conversion: Array = [] # Array[Dictionary] 键 attr
|
||||
|
||||
const FORGE_ATTRS := ["max_hp", "atk", "armor_class"]
|
||||
const WORKSHOP_ITEM_IDS := ["probe_dust", "sapper_order", "loot_compass", "nest_pendulum", "catacomb_scroll", "blast_kit"]
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
# 行族空护栏(四族皆为 ConfigDB 消费面——缺族即致命)
|
||||
for family in ["forge_products", "boss_keystone", "workshop_items", "conversion"]:
|
||||
if get(family).is_empty():
|
||||
errs.append({"category": "empty_table", "message": "MetaTable 行族为空: %s" % family})
|
||||
# forge_products:attr 词表封闭 + 唯一 + 覆盖恰三(注入契约三字段必齐)+ flat/价格域 + 公比域 [100,400]
|
||||
var seen_attr := {}
|
||||
for row in forge_products:
|
||||
var attr: String = row.get("attr", "")
|
||||
if not FORGE_ATTRS.has(attr):
|
||||
errs.append({"category": "enum", "message": "forge_products attr 出域 {max_hp, atk, armor_class}: %s" % attr})
|
||||
if seen_attr.has(attr):
|
||||
errs.append({"category": "dup_key", "message": "forge_products attr 重复: %s" % attr})
|
||||
seen_attr[attr] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "forge_products %s display_name 空" % attr})
|
||||
var flat: int = int(row.get("flat_value", 0))
|
||||
if flat < 1 or flat > 999:
|
||||
errs.append({"category": "domain", "message": "forge_products %s flat_value 越域 [1,999]: %d" % [attr, flat]})
|
||||
var base: int = int(row.get("base_price", 0))
|
||||
if base < 1 or base > 999:
|
||||
errs.append({"category": "domain", "message": "forge_products %s base_price 越域 [1,999]: %d" % [attr, base]})
|
||||
var ratio: int = int(row.get("price_ratio_pct", -1))
|
||||
if ratio < 100 or ratio > 400:
|
||||
errs.append({"category": "domain", "message": "forge_products %s price_ratio_pct 越域 [100,400]: %d" % [attr, ratio]})
|
||||
if not forge_products.is_empty():
|
||||
for attr_key in FORGE_ATTRS:
|
||||
if not seen_attr.has(attr_key):
|
||||
errs.append({"category": "coverage", "message": "forge_products 缺属性键: %s(注入契约三字段必齐——恰三)" % attr_key})
|
||||
# boss_keystone:level_index 唯一 + 连续从 1 + 两档域 + first ≥ repeat(首杀全量、重复小额恒定)
|
||||
var seen_li := {}
|
||||
for row in boss_keystone:
|
||||
var li: int = int(row.get("level_index", 0))
|
||||
if seen_li.has(li):
|
||||
errs.append({"category": "dup_key", "message": "boss_keystone level_index 重复: %d" % li})
|
||||
seen_li[li] = true
|
||||
var first: int = int(row.get("first_kill", -1))
|
||||
var rep: int = int(row.get("repeat_kill", -1))
|
||||
if first < 1 or first > 999 or rep < 0 or rep > 999:
|
||||
errs.append({"category": "domain", "message": "boss_keystone L%d 两档金额越域 first [1,999] / repeat [0,999]: %d/%d" % [li, first, rep]})
|
||||
if first < rep:
|
||||
errs.append({"category": "cross_field", "message": "boss_keystone L%d 违反 first_kill(%d) ≥ repeat_kill(%d)——首杀全量、重复杀小额" % [li, first, rep]})
|
||||
for i in boss_keystone.size():
|
||||
var li_at: int = int(boss_keystone[i].get("level_index", 0))
|
||||
if li_at != i + 1:
|
||||
errs.append({"category": "structural", "message": "boss_keystone level_index 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, li_at]})
|
||||
break
|
||||
# workshop_items:item_id 词表封闭 + 唯一 + 覆盖恰六(registry aux_items 词表)+ 阈值域 [1,99]
|
||||
var seen_item := {}
|
||||
for row in workshop_items:
|
||||
var iid: String = row.get("item_id", "")
|
||||
if not WORKSHOP_ITEM_IDS.has(iid):
|
||||
errs.append({"category": "enum", "message": "workshop_items item_id 出域 aux_items 六件词表: %s" % iid})
|
||||
if seen_item.has(iid):
|
||||
errs.append({"category": "dup_key", "message": "workshop_items item_id 重复: %s" % iid})
|
||||
seen_item[iid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "workshop_items %s display_name 空" % iid})
|
||||
var th: int = int(row.get("clear_threshold", -1))
|
||||
if th < 1 or th > 99:
|
||||
errs.append({"category": "domain", "message": "workshop_items %s clear_threshold 越域 [1,99]: %d" % [iid, th]})
|
||||
if not workshop_items.is_empty():
|
||||
for iid_key in WORKSHOP_ITEM_IDS:
|
||||
if not seen_item.has(iid_key):
|
||||
errs.append({"category": "coverage", "message": "workshop_items 缺道具键: %s(六件词表恰全)" % iid_key})
|
||||
# conversion:attr 词表与 forge 一致 + 唯一 + 覆盖恰三 + 平移量域 [0,100]
|
||||
var seen_conv := {}
|
||||
for row in conversion:
|
||||
var cattr: String = row.get("attr", "")
|
||||
if not FORGE_ATTRS.has(cattr):
|
||||
errs.append({"category": "enum", "message": "conversion attr 出域 {max_hp, atk, armor_class}: %s" % cattr})
|
||||
if seen_conv.has(cattr):
|
||||
errs.append({"category": "dup_key", "message": "conversion attr 重复: %s" % cattr})
|
||||
seen_conv[cattr] = true
|
||||
var shift: int = int(row.get("per_level_shift_pct", -1))
|
||||
if shift < 0 or shift > 100:
|
||||
errs.append({"category": "domain", "message": "conversion %s per_level_shift_pct 越域 [0,100]: %d" % [cattr, shift]})
|
||||
if not conversion.is_empty():
|
||||
for conv_key in FORGE_ATTRS:
|
||||
if not seen_conv.has(conv_key):
|
||||
errs.append({"category": "coverage", "message": "conversion 缺属性行: %s(三行与 forge 词表一致)" % conv_key})
|
||||
return errs
|
||||
@@ -0,0 +1 @@
|
||||
uid://b2nl5v8gb6cw
|
||||
@@ -0,0 +1,80 @@
|
||||
[gd_resource type="Resource" script_class="MetaTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/meta_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
forge_products = [{
|
||||
"attr": "max_hp",
|
||||
"display_name": "生命锻造",
|
||||
"flat_value": 3,
|
||||
"base_price": 10,
|
||||
"price_ratio_pct": 150
|
||||
}, {
|
||||
"attr": "atk",
|
||||
"display_name": "攻击锻造",
|
||||
"flat_value": 1,
|
||||
"base_price": 10,
|
||||
"price_ratio_pct": 150
|
||||
}, {
|
||||
"attr": "armor_class",
|
||||
"display_name": "护甲锻造",
|
||||
"flat_value": 1,
|
||||
"base_price": 10,
|
||||
"price_ratio_pct": 150
|
||||
}]
|
||||
boss_keystone = [{
|
||||
"level_index": 1,
|
||||
"first_kill": 5,
|
||||
"repeat_kill": 1
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"first_kill": 8,
|
||||
"repeat_kill": 2
|
||||
}, {
|
||||
"level_index": 3,
|
||||
"first_kill": 12,
|
||||
"repeat_kill": 3
|
||||
}]
|
||||
workshop_items = [{
|
||||
"item_id": "probe_dust",
|
||||
"display_name": "显影尘",
|
||||
"clear_threshold": 1
|
||||
}, {
|
||||
"item_id": "sapper_order",
|
||||
"display_name": "工兵令",
|
||||
"clear_threshold": 1
|
||||
}, {
|
||||
"item_id": "loot_compass",
|
||||
"display_name": "探宝罗盘",
|
||||
"clear_threshold": 2
|
||||
}, {
|
||||
"item_id": "nest_pendulum",
|
||||
"display_name": "探巢摆钟",
|
||||
"clear_threshold": 2
|
||||
}, {
|
||||
"item_id": "catacomb_scroll",
|
||||
"display_name": "地穴全图卷",
|
||||
"clear_threshold": 3
|
||||
}, {
|
||||
"item_id": "blast_kit",
|
||||
"display_name": "爆破工兵包",
|
||||
"clear_threshold": 3
|
||||
}]
|
||||
conversion = [{
|
||||
"attr": "max_hp",
|
||||
"per_level_shift_pct": 0
|
||||
}, {
|
||||
"attr": "atk",
|
||||
"per_level_shift_pct": 0
|
||||
}, {
|
||||
"attr": "armor_class",
|
||||
"per_level_shift_pct": 0
|
||||
}]
|
||||
|
||||
; 元进度实现批(2026-09-21,设计权威 design/gdd/systems/冒险循环/元进度.md 定稿 v1.2):
|
||||
; 数值全 provisional 归元进度设计批配读数(骨架先行——元进度实现批 2026-09-21,先例 = GP+商店房批)。
|
||||
; forge flat 档与 GP 商店同族(血+3/攻+1/甲+1),base 10 / ratio 150 三行同;boss 两档 L1 5/1 · L2 8/2 · L3 12/3
|
||||
; (首杀全量、重复杀固定小额恒定不递减);工坊六件阈值 1/1/2/2/3/3 按总通关次数(首版只挂通关次数渠道,
|
||||
; 钥石直购价随定价批后补);conversion 全 0 = 未标定占位(标定归设计批,护甲行强条目:验收读数必须按
|
||||
; 解锁后参数重测)。
|
||||
@@ -102,3 +102,20 @@ static func adventure_summary_text(summary: Dictionary) -> String:
|
||||
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))]
|
||||
|
||||
|
||||
# 钥石入账明细文案(元进度实现批——settle_adventure 明细的结算屏追加段:大奖励 / 逐关 Boss
|
||||
# 首杀或重复两档 / 新解锁道具;name_lookup = item_id → 显示名投影,由组合根自 meta 表喂入)
|
||||
static func keystone_settle_text(detail: Dictionary, name_lookup: Dictionary = {}) -> String:
|
||||
var parts: Array = ["大奖励 +%d" % int(detail.get("big_reward", 0))]
|
||||
for d in detail.get("boss_drops", []):
|
||||
parts.append("第 %d 关 Boss %s +%d" % [int(d.get("level", 0)),
|
||||
"首杀" if bool(d.get("first", false)) else "重复杀", int(d.get("amount", 0))])
|
||||
var line := "钥石入账:" + " · ".join(parts)
|
||||
var unlocks: Array = detail.get("new_unlocks", [])
|
||||
if not unlocks.is_empty():
|
||||
var names: Array = []
|
||||
for iid in unlocks:
|
||||
names.append(String(name_lookup.get(String(iid), String(iid))))
|
||||
line += "\n工坊新解锁:" + "、".join(names)
|
||||
return line + "\n本局入账合计 +%d 钥石" % int(detail.get("total", 0))
|
||||
@@ -16,10 +16,14 @@ extends RefCounted
|
||||
|
||||
# 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配)
|
||||
static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView,
|
||||
settlement, runner, level_active: bool, db, refresh_remaining: int, gp_label: Label = null) -> void:
|
||||
settlement, runner, level_active: bool, db, refresh_remaining: int, gp_label: Label = null,
|
||||
keystone_label: Label = null, keystone_balance: int = 0) -> void:
|
||||
# GP 余额常驻(GP+商店房实施批 2026-09-21——A 区顶栏邻接位;runner 缺位 = 非冒险态按 0 读数)
|
||||
if gp_label != null:
|
||||
gp_label.text = "金币 %d" % (int(runner.gp) if runner != null else 0)
|
||||
# 钥石余额常驻(元进度实现批 2026-09-21——A 区顶栏邻接位;数据源 MetaProgression 账本,购买/收束后随全量重查刷新)
|
||||
if keystone_label != null:
|
||||
keystone_label.text = "钥石 %d" % keystone_balance
|
||||
if settlement == null:
|
||||
_refresh_out_of_battle(stage, info, runner, level_active, db)
|
||||
return
|
||||
|
||||
@@ -27,12 +27,14 @@ const ModalStack := preload("res://presentation/modal_stack.gd")
|
||||
const TuneSimController := preload("res://presentation/tune_sim_controller.gd")
|
||||
const DebugSessionScript := preload("res://presentation/debug_session.gd")
|
||||
const SaveManagerScript := preload("res://core/save_manager.gd") # 存档承载(preload 避开全局类缓存时序——与 DebugSessionScript 同款)
|
||||
const MetaProgressionScript := preload("res://core/meta_progression.gd") # 元进度账本服务(元进度实现批——同 preload 纪律)
|
||||
|
||||
var db
|
||||
var settlement = null # 当前战斗编排器(关卡与调试模式统一读它)
|
||||
var runner = null # 关卡节点地图 图版推进器(完整关卡模式;冒险模式 = 每关重取 adventure.runner())
|
||||
var adventure = null # 冒险结构编排器(冒险结构实现批——三关连探;转发信号照 runner 接线原样对接)
|
||||
var save_manager = null # 存档序列化承载(存档序列化批——启动读盘/冒险收束写盘;只经访问器与 apply 原语,禁读内部结构)
|
||||
var meta = null # 元进度账本服务(元进度实现批——余额/定价/购买/工坊解锁/Boss两档入账/注入计算;零信号纯被动)
|
||||
var _adventure_mode := false # 冒险模式标记(inter 层确认分派与终局分派)
|
||||
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
|
||||
var _level_active := false
|
||||
@@ -56,11 +58,13 @@ var board_view: BoardView # 探针兼容别名(= board_zone.bo
|
||||
var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar)
|
||||
var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下)
|
||||
var gp_label: Label # A 区顶栏 GP 余额常驻位(GP+商店房实施批——文案装配归 HudPresenter)
|
||||
var keystone_label: Label # A 区顶栏钥石余额常驻位(元进度实现批——文案装配归 HudPresenter)
|
||||
# 模态 overlay(z 序 = 登记序;pattern 6 层级纪律)
|
||||
var legend_overlay: LegendOverlay
|
||||
var review_overlay: SettlementReviewOverlay
|
||||
var node_overlay: NodeOverlay
|
||||
var shop_overlay: ShopOverlay # 商店覆盖层(GP+商店房实施批——商店房节点兑付呈现)
|
||||
var camp_overlay: CampOverlay # 营地覆盖层(元进度实现批——主菜单营地入口兑付呈现)
|
||||
var tune_panel: TunePanel
|
||||
|
||||
var _modals = null # 模态层级状态机(modal_stack.gd——缺陷⑤纪律载体)
|
||||
@@ -82,6 +86,8 @@ func _ready() -> void:
|
||||
# (fatal 只留给数值表——fail-fast 边界在表加载,不在档);高版本拒读态后续写盘被类内拒绝。
|
||||
save_manager = SaveManagerScript.new()
|
||||
save_manager.load_from_disk()
|
||||
# 元进度账本(元进度实现批):存档就绪后建——余额/定价/购买/入账/注入全经此单点
|
||||
meta = MetaProgressionScript.new(db, save_manager)
|
||||
_debug = DebugSessionScript.new(db, get_node("/root/RngService"),
|
||||
Callable(self, "_debug_hero_override"), Callable(self, "_seed_value"))
|
||||
_debug.battle_created.connect(_on_debug_battle_created)
|
||||
@@ -98,6 +104,7 @@ func _build_ui() -> void:
|
||||
var battle_ui: Dictionary = UiAssembler.build_battle_ui(root, db)
|
||||
strip_view = battle_ui.strip_view
|
||||
gp_label = battle_ui.gp_label
|
||||
keystone_label = battle_ui.keystone_label
|
||||
stage = battle_ui.stage
|
||||
log_view = battle_ui.log_view
|
||||
info_panel = battle_ui.info_panel
|
||||
@@ -128,6 +135,7 @@ func _build_ui() -> void:
|
||||
node_overlay = overlays.node_overlay
|
||||
_end_overlay = overlays.end_overlay
|
||||
shop_overlay = overlays.shop_overlay
|
||||
camp_overlay = overlays.camp_overlay
|
||||
tune_panel = overlays.tune_panel
|
||||
route_overlay = RouteOverlay.new()
|
||||
add_child(route_overlay)
|
||||
@@ -140,13 +148,15 @@ func _build_ui() -> void:
|
||||
route_overlay.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119)
|
||||
shop_overlay.buy_requested.connect(_on_shop_buy)
|
||||
shop_overlay.leave_pressed.connect(_on_shop_leave)
|
||||
camp_overlay.buy_requested.connect(_on_camp_buy)
|
||||
camp_overlay.closed.connect(_on_camp_closed)
|
||||
_end_overlay.review_pressed.connect(_on_end_review)
|
||||
_end_overlay.continue_pressed.connect(_on_battle_end_continue)
|
||||
tune_panel.closed.connect(_close_tune_panel)
|
||||
tune_panel.apply_requested.connect(_on_tune_apply)
|
||||
tune_panel.simulate_requested.connect(_on_tune_simulate)
|
||||
_modals = ModalStack.new()
|
||||
for m: Control in [legend_overlay, review_overlay, node_overlay, route_overlay, _end_overlay, shop_overlay, tune_panel]:
|
||||
for m: Control in [legend_overlay, review_overlay, node_overlay, route_overlay, _end_overlay, shop_overlay, camp_overlay, tune_panel]:
|
||||
_register_modal(m)
|
||||
_tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp))
|
||||
|
||||
@@ -158,6 +168,7 @@ func _build_ui() -> void:
|
||||
start.lv3btn.pressed.connect(_start_full_level.bind(3))
|
||||
start.tunebtn.pressed.connect(_show_tune_panel)
|
||||
start.dbgbtn.pressed.connect(_start_debug_levels)
|
||||
start.campbtn.pressed.connect(_open_camp)
|
||||
|
||||
var inter: Dictionary = UiAssembler.build_inter_overlay(self)
|
||||
_inter_overlay = inter.overlay
|
||||
@@ -258,7 +269,8 @@ func _start_adventure() -> void:
|
||||
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):
|
||||
# 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板)
|
||||
if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}}):
|
||||
_fatal_screen("冒险启动失败——见输出日志")
|
||||
return
|
||||
|
||||
@@ -342,7 +354,7 @@ func _on_big_reward(reward: Dictionary) -> void:
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
# 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事进文案
|
||||
# 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案
|
||||
func _on_adventure_finished(summary: Dictionary) -> void:
|
||||
_level_active = false
|
||||
var result: String = String(summary.get("result", ""))
|
||||
@@ -351,18 +363,19 @@ func _on_adventure_finished(summary: Dictionary) -> void:
|
||||
_sync_settlement_visuals() # → 全关
|
||||
info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④)
|
||||
stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面)
|
||||
# 存档入账(元进度实现批):settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变——
|
||||
# 单关测试入口与调试两层不经过本处理器,天然不写档不污染生涯数据)。
|
||||
var detail: Dictionary = {}
|
||||
if meta != null:
|
||||
detail = meta.settle_adventure(summary)
|
||||
var text := Narrator.adventure_summary_text(summary)
|
||||
if not detail.is_empty():
|
||||
text += "\n" + Narrator.keystone_settle_text(detail, _item_name_lookup())
|
||||
if result == "completed":
|
||||
_end_overlay.open_victory(Narrator.adventure_summary_text(summary),
|
||||
"返回冒险", board_view.get_global_rect())
|
||||
_end_overlay.open_victory(text, "返回冒险", board_view.get_global_rect())
|
||||
else:
|
||||
_end_overlay.open_defeat(Narrator.adventure_summary_text(summary),
|
||||
"返回", board_view.get_global_rect())
|
||||
_end_overlay.open_defeat(text, "返回", board_view.get_global_rect())
|
||||
logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷)
|
||||
# 存档入账(存档序列化批):正式冒险收束唯一写档点——域更新原语 + 原子写盘。
|
||||
# 单关测试入口与调试两层不经过本处理器,天然不写档(契约:测试模式不污染生涯数据)。
|
||||
if save_manager != null:
|
||||
save_manager.apply_adventure_summary(summary)
|
||||
save_manager.write_to_disk()
|
||||
if runner != null:
|
||||
runner.restore_hero_base()
|
||||
strip_view.refresh(runner)
|
||||
@@ -370,6 +383,15 @@ func _on_adventure_finished(summary: Dictionary) -> void:
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
# 工坊 item_id → 显示名投影(keystone_settle_text 文案数据源——装配归本根,拼句归 Narrator)
|
||||
func _item_name_lookup() -> Dictionary:
|
||||
var lookup := {}
|
||||
if meta != null:
|
||||
for row in db.meta_workshop_items():
|
||||
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
|
||||
return lookup
|
||||
|
||||
|
||||
func _on_battle_end_continue() -> void:
|
||||
# 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石已在收束时入存档——存档序列化批)
|
||||
_close_modal(_end_overlay)
|
||||
@@ -622,6 +644,57 @@ func _on_abandon_requested() -> void:
|
||||
board_zone.toast.show_toast("战斗进行中——无法放弃", "warn")
|
||||
|
||||
|
||||
# ---- 营地覆盖层与钥石账本(元进度实现批——写调用唯一归组合根)----
|
||||
|
||||
# 打开营地(主菜单入口——开合与开始层互斥仿 TunePanel 先例:开始覆盖层非模态且树序在后,
|
||||
# 规范 2.3 遮挡纪律;打开时先刷工坊解锁——达标未解锁就地入账)
|
||||
func _open_camp() -> void:
|
||||
if meta == null:
|
||||
return
|
||||
meta.refresh_workshop_unlocks()
|
||||
_start_overlay.visible = false
|
||||
camp_overlay.open_camp(_camp_data())
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
func _on_camp_closed() -> void:
|
||||
camp_overlay.visible = false
|
||||
_start_overlay.visible = true
|
||||
_refresh_presentations() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面)
|
||||
|
||||
|
||||
# 营地快照投影(纯读——锻造三行/工坊六行/余额/最近冒险;拼句归 CampOverlay,账面归 MetaProgression)
|
||||
func _camp_data() -> Dictionary:
|
||||
var forge_rows: Array = []
|
||||
if meta != null:
|
||||
for attr in MetaTable.FORGE_ATTRS:
|
||||
var row: Dictionary = db.meta_forge_row(attr)
|
||||
forge_rows.append({
|
||||
"attr": attr,
|
||||
"display_name": String(row.get("display_name", "")),
|
||||
"level": meta.forge_level(attr),
|
||||
"flat": int(row.get("flat_value", 0)),
|
||||
"price": meta.forge_price(attr),
|
||||
})
|
||||
return {
|
||||
"balance": meta.keystone_balance() if meta != null else 0,
|
||||
"forge": forge_rows,
|
||||
"workshop": meta.workshop_status() if meta != null else [],
|
||||
"recent": save_manager.recent_summaries() if save_manager != null else [],
|
||||
}
|
||||
|
||||
|
||||
func _on_camp_buy(attr: String) -> void:
|
||||
if meta == null:
|
||||
return
|
||||
if not meta.buy_forge(attr):
|
||||
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
|
||||
board_zone.toast.show_toast("钥石不足——买不起这件强化", "warn")
|
||||
return
|
||||
camp_overlay.refresh_camp(_camp_data())
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
# ---- 商店覆盖层与 GP 钱包(GP+商店房实施批——写调用唯一归组合根)----
|
||||
|
||||
func _on_shop_buy(product_id: String) -> void:
|
||||
@@ -728,7 +801,8 @@ func _handle_outcome_feedback(outcome: Dictionary) -> void:
|
||||
func _refresh_presentations() -> void:
|
||||
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
|
||||
var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_round)
|
||||
HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining, gp_label)
|
||||
HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining,
|
||||
gp_label, keystone_label, meta.keystone_balance() if meta != null else 0)
|
||||
_sync_settlement_visuals()
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ static func build_battle_ui(root: Control, db) -> Dictionary:
|
||||
var gp_label := UiKit.label("金币 0", 15, true)
|
||||
gp_label.add_theme_color_override("font_color", UiKit.GOLD_DEEP)
|
||||
strip_row.add_child(gp_label)
|
||||
var keystone_label := UiKit.label("钥石 0", 15, true) # 元进度实现批——A 区顶栏钥石余额常驻(文案装配归 HudPresenter,此处只建节点)
|
||||
keystone_label.add_theme_color_override("font_color", UiKit.GOLD)
|
||||
strip_row.add_child(keystone_label)
|
||||
|
||||
# B 区:战斗画面(全宽 × 252px——常驻双舞台,布局决策 D1)
|
||||
var stage := BattleStageView.new(db)
|
||||
@@ -56,6 +59,7 @@ static func build_battle_ui(root: Control, db) -> Dictionary:
|
||||
return {
|
||||
"strip_view": strip_view,
|
||||
"gp_label": gp_label,
|
||||
"keystone_label": keystone_label,
|
||||
"stage": stage,
|
||||
"log_view": stage.log_view,
|
||||
"info_panel": info_panel,
|
||||
@@ -85,6 +89,10 @@ static func build_overlays(host: Control, db, base_atk: int, base_hp: int) -> Di
|
||||
var shop_overlay := ShopOverlay.new()
|
||||
host.add_child(shop_overlay)
|
||||
|
||||
# 营地覆盖层(元进度实现批 2026-09-21——主菜单营地入口兑付呈现;开合仿 TunePanel 与开始层互斥)
|
||||
var camp_overlay := CampOverlay.new()
|
||||
host.add_child(camp_overlay)
|
||||
|
||||
var tune_panel := TunePanel.new(db, base_atk, base_hp)
|
||||
tune_panel.visible = false # 显式初始化——原版依赖"被不透明开始层遮蔽"的树序巧合(缺陷⑤家族);模态锁输入以 visible 为准
|
||||
host.add_child(tune_panel)
|
||||
@@ -95,6 +103,7 @@ static func build_overlays(host: Control, db, base_atk: int, base_hp: int) -> Di
|
||||
"node_overlay": node_overlay,
|
||||
"end_overlay": end_overlay,
|
||||
"shop_overlay": shop_overlay,
|
||||
"camp_overlay": camp_overlay,
|
||||
"tune_panel": tune_panel,
|
||||
}
|
||||
|
||||
@@ -131,6 +140,8 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary:
|
||||
box.add_child(tunebtn)
|
||||
var dbgbtn := UiKit.button("调试 · 连续关卡两层(HP 关内延续)")
|
||||
box.add_child(dbgbtn)
|
||||
var campbtn := UiKit.button("营地 · 永久强化(钥石换永久属性——跨局成长账本)") # 元进度实现批——主菜单营地入口(双入口框架的主菜单一侧)
|
||||
box.add_child(campbtn)
|
||||
|
||||
# 玩法帮助文案(表值驱动——2026-09-16 AP 压速批遗留硬编码 8 随手修正;护甲等级掷点批同步"减伤"→"护甲强化")
|
||||
var help_text := "玩法:每轮 %d AP——挖开(1AP)/插旗(1AP)/移动(免费)/连锁(免费)。\n每个非封锁格都藏一种格面资源(增伤/护甲强化/回血/爆裂/削弱)。开安全格、标中雷 = 正向兑付;\n开雷格、标错安全格 = 负向兑付——全部在轮末结算逐行兑现;吃掷点的基础攻击按护甲等级判定,无 miss、最低保底 1 点。\n停手:剩余 AP 转存 RAP;弃图刷新:RAP−%d 当场换图继续挖(每轮剩 %d 次)。\n敌人全灭胜、主角倒下负。悬停可看三态光标与可挖域微光。" % [
|
||||
@@ -140,7 +151,8 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary:
|
||||
box.add_child(help)
|
||||
|
||||
return {"overlay": overlay, "box": box, "seed_edit": seed_edit,
|
||||
"lvbtn": lvbtn, "lv2btn": lv2btn, "lv3btn": lv3btn, "tunebtn": tunebtn, "dbgbtn": dbgbtn}
|
||||
"lvbtn": lvbtn, "lv2btn": lv2btn, "lv3btn": lv3btn, "tunebtn": tunebtn, "dbgbtn": dbgbtn,
|
||||
"campbtn": campbtn}
|
||||
|
||||
|
||||
# 调试两层模式的层间过渡覆盖层(历史入口专用)。返回 {overlay, label, next_btn}——接线归组合根
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-21(元进度实现批——营地覆盖层呈现)
|
||||
# 营地覆盖层(模态):标题「营地 · 永久强化」+ 钥石余额 / 柜台一「属性锻造」(三行可购——显示名 ·
|
||||
# 当前 Lv · 每级 +flat · 下级价格 · 购买按钮余额不足禁用)/ 柜台二「道具工坊」(六行——显示名 ·
|
||||
# 解锁条件 · 进度 · 状态;局内效果排 Alpha 魔法道具重设计批——代拍板 3,注记一行)/ 柜台三
|
||||
# 「奖励格图鉴」(占位锁定行——内容排 Alpha 地图战斗资源差异化批——代拍板 4)/「最近冒险」列表
|
||||
# (recent_summaries 投影:结果/通关数/钥石合计/种子)/ 关闭按钮。
|
||||
# 挂接方式:仿 ShopOverlay(dim + CenterContainer + box);开合与开始层互斥仿 TunePanel 先例
|
||||
# (开始覆盖层非模态且树序在后——打开本层前由组合根隐藏开始层,规范 2.3 遮挡纪律)。
|
||||
# 高内聚边界:纯视图 setter 面——账本快照经组合根喂入(MetaProgression 只读投影),购买/关闭
|
||||
# 意图经信号外发——零核心写调用、零 MetaProgression 持有。视觉 = ui_kit 程序占位
|
||||
#(代拍板 8——视觉终稿以表现层样张过审为前置,归 HUD 协作批)。
|
||||
class_name CampOverlay
|
||||
extends Control
|
||||
|
||||
signal buy_requested(attr: String)
|
||||
signal closed
|
||||
|
||||
const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表,与 ShopOverlay 同款)
|
||||
|
||||
var _balance_label: Label
|
||||
var _forge_holder: VBoxContainer
|
||||
var _workshop_holder: VBoxContainer
|
||||
var _forge_rows: Array = [] # [{attr, level_l, price_l, btn}]——refresh_camp 就地重刷(零重建回流)
|
||||
var _workshop_rows: Array = [] # [{item_id, progress_l, status_l}]
|
||||
var _recent_list: Label # 最近冒险(逐行拼接 label——只读账面,随 open_camp 重建)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var dim := ColorRect.new()
|
||||
dim.color = Color(0, 0, 0, 0.72)
|
||||
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(dim)
|
||||
var center := CenterContainer.new()
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
dim.add_child(center)
|
||||
var box := VBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", 6)
|
||||
center.add_child(box)
|
||||
var title := UiKit.label("营地 · 永久强化", 24, true)
|
||||
title.add_theme_color_override("font_color", UiKit.WARM)
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(title)
|
||||
_balance_label = UiKit.label("钥石 0", 18, true)
|
||||
_balance_label.add_theme_color_override("font_color", UiKit.GOLD)
|
||||
_balance_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(_balance_label)
|
||||
box.add_child(HSeparator.new())
|
||||
|
||||
box.add_child(_counter_header("属性锻造(钥石直购 · 每级 flat 整数 · 价格几何递增)"))
|
||||
_forge_holder = VBoxContainer.new()
|
||||
_forge_holder.add_theme_constant_override("separation", 4)
|
||||
box.add_child(_forge_holder)
|
||||
|
||||
box.add_child(_counter_header("道具工坊(累计通关次数解锁 · 局内效果排 Alpha 魔法道具重设计批)"))
|
||||
_workshop_holder = VBoxContainer.new()
|
||||
_workshop_holder.add_theme_constant_override("separation", 4)
|
||||
box.add_child(_workshop_holder)
|
||||
|
||||
box.add_child(_counter_header("奖励格图鉴"))
|
||||
var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)", 13, false)
|
||||
lock_l.add_theme_color_override("font_color", UiKit.WARM_FAINT)
|
||||
box.add_child(lock_l)
|
||||
box.add_child(HSeparator.new())
|
||||
|
||||
box.add_child(_counter_header("最近冒险"))
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.custom_minimum_size = Vector2(560, 96)
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
box.add_child(scroll)
|
||||
_recent_list = UiKit.label("", 13, false)
|
||||
_recent_list.add_theme_color_override("font_color", UiKit.WARM_SOFT)
|
||||
_recent_list.custom_minimum_size = Vector2(544, 0)
|
||||
_recent_list.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||
scroll.add_child(_recent_list)
|
||||
|
||||
var close_btn := UiKit.button("关闭营地(回开始界面)")
|
||||
close_btn.pressed.connect(func(): closed.emit())
|
||||
box.add_child(close_btn)
|
||||
visible = false
|
||||
|
||||
|
||||
func _counter_header(text: String) -> Label:
|
||||
var l := UiKit.label(text, 15, true)
|
||||
l.add_theme_color_override("font_color", UiKit.WARM_SOFT)
|
||||
return l
|
||||
|
||||
|
||||
# ---- 柜台行构建(open_camp 重建;行结构仿 ShopOverlay 商品卡)----
|
||||
|
||||
# 打开营地(全量重建三柜台行 + 最近冒险——数据 = 组合根 _camp_data() 投影)
|
||||
func open_camp(data: Dictionary) -> void:
|
||||
_rebuild_forge(data)
|
||||
_rebuild_workshop(data)
|
||||
_rebuild_recent(data)
|
||||
refresh_camp(data)
|
||||
visible = true
|
||||
|
||||
|
||||
# 状态重刷(购买后调用——余额/价格/禁用态/工坊状态就地改文案,零重建)
|
||||
func refresh_camp(data: Dictionary) -> void:
|
||||
var balance := int(data.get("balance", 0))
|
||||
_balance_label.text = "钥石 %d" % balance
|
||||
var forge_by_attr := {}
|
||||
for row in data.get("forge", []):
|
||||
forge_by_attr[String(row.get("attr", ""))] = row
|
||||
for card in _forge_rows:
|
||||
var row: Dictionary = forge_by_attr.get(String(card.attr), {})
|
||||
if row.is_empty():
|
||||
continue
|
||||
card.level_l.text = "Lv %d" % int(row.get("level", 0))
|
||||
card.price_l.text = "下级价格 %d 钥石" % int(row.get("price", 0))
|
||||
card.btn.disabled = balance < int(row.get("price", 0)) # 余额不足禁用置灰
|
||||
var ws_by_id := {}
|
||||
for row in data.get("workshop", []):
|
||||
ws_by_id[String(row.get("item_id", ""))] = row
|
||||
for card in _workshop_rows:
|
||||
var row: Dictionary = ws_by_id.get(String(card.item_id), {})
|
||||
if row.is_empty():
|
||||
continue
|
||||
var th := int(row.get("threshold", 0))
|
||||
var prog := int(row.get("progress", 0))
|
||||
var unlocked := bool(row.get("unlocked", false))
|
||||
card.progress_l.text = "累计通关 %d / %d 次" % [prog, th]
|
||||
card.status_l.text = "已解锁" if unlocked else "未解锁"
|
||||
card.status_l.add_theme_color_override("font_color", UiKit.GOLD if unlocked else UiKit.WARM_FAINT)
|
||||
|
||||
|
||||
func _rebuild_forge(data: Dictionary) -> void:
|
||||
_forge_rows.clear()
|
||||
for child in _forge_holder.get_children():
|
||||
child.queue_free()
|
||||
for row in data.get("forge", []):
|
||||
_forge_rows.append(_build_forge_row(_forge_holder, row))
|
||||
|
||||
|
||||
func _build_forge_row(holder: VBoxContainer, row: Dictionary) -> Dictionary:
|
||||
var card := PanelContainer.new()
|
||||
card.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.BG_OVERLAY, UiKit.GOLD_DIM, 4, 1, Vector4(12, 4, 12, 4)))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 12)
|
||||
card.add_child(line)
|
||||
var attr := String(row.get("attr", ""))
|
||||
var name_l := UiKit.label("%s(每级 %+d flat)" % [String(row.get("display_name", "")), int(row.get("flat", 0))], 14, true)
|
||||
name_l.custom_minimum_size = Vector2(210, 0)
|
||||
line.add_child(name_l)
|
||||
var level_l := UiKit.label("", 14, false)
|
||||
level_l.custom_minimum_size = Vector2(64, 0)
|
||||
line.add_child(level_l)
|
||||
var price_l := UiKit.label("", 14, false)
|
||||
price_l.add_theme_color_override("font_color", UiKit.WARM_SOFT)
|
||||
price_l.custom_minimum_size = Vector2(150, 0)
|
||||
line.add_child(price_l)
|
||||
var btn := UiKit.button("锻造 +1 级")
|
||||
btn.pressed.connect(func(): buy_requested.emit(attr))
|
||||
line.add_child(btn)
|
||||
holder.add_child(card)
|
||||
return {"attr": attr, "level_l": level_l, "price_l": price_l, "btn": btn}
|
||||
|
||||
|
||||
func _rebuild_workshop(data: Dictionary) -> void:
|
||||
_workshop_rows.clear()
|
||||
for child in _workshop_holder.get_children():
|
||||
child.queue_free()
|
||||
for row in data.get("workshop", []):
|
||||
var card := PanelContainer.new()
|
||||
card.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.BG_OVERLAY, UiKit.WARM_FAINT, 4, 1, Vector4(12, 4, 12, 4)))
|
||||
var line := HBoxContainer.new()
|
||||
line.add_theme_constant_override("separation", 12)
|
||||
card.add_child(line)
|
||||
var name_l := UiKit.label(String(row.get("display_name", "")), 14, true)
|
||||
name_l.custom_minimum_size = Vector2(210, 0)
|
||||
line.add_child(name_l)
|
||||
var progress_l := UiKit.label("", 13, false)
|
||||
progress_l.add_theme_color_override("font_color", UiKit.WARM_SOFT)
|
||||
progress_l.custom_minimum_size = Vector2(150, 0)
|
||||
line.add_child(progress_l)
|
||||
var status_l := UiKit.label("", 13, true)
|
||||
line.add_child(status_l)
|
||||
_workshop_holder.add_child(card)
|
||||
_workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l})
|
||||
|
||||
|
||||
func _rebuild_recent(data: Dictionary) -> void:
|
||||
var lines: Array = []
|
||||
var recent: Array = data.get("recent", [])
|
||||
if recent.is_empty():
|
||||
lines.append(" (暂无冒险记录——完成或结束一次冒险后入账)")
|
||||
for s in recent:
|
||||
var verdict := "失败"
|
||||
if String(s.get("result", "")) == "completed":
|
||||
verdict = "完成"
|
||||
elif String(s.get("result", "")) == "abandoned":
|
||||
verdict = "放弃"
|
||||
lines.append("%s · 通关 %d 关 · 钥石 +%d · 种子 %d" % [verdict,
|
||||
int(s.get("levels_cleared", 0)), int(s.get("keystone_total", 0)), int(s.get("master_seed", 0))])
|
||||
_recent_list.text = "\n".join(lines)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dvg2wmjh87p8a
|
||||
@@ -102,6 +102,7 @@ func _run_core_smoke(db, rng) -> void:
|
||||
_smoke_adventure_api()
|
||||
_smoke_adventure()
|
||||
_smoke_save()
|
||||
_smoke_meta()
|
||||
|
||||
|
||||
# ---- 冒险结构实现批冒烟(2026-09-21——AdventureTable 建表 + AdventureRunner + 冒险验收断言)----
|
||||
@@ -343,6 +344,7 @@ func _adv_st() -> Dictionary:
|
||||
"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 区边框〕)----
|
||||
@@ -3063,6 +3065,198 @@ func _smoke_nine_types_gp_stream() -> void:
|
||||
check(rng.counter("gp_drop") == 0, "derive_at 纯函数不推计数器(gp_drop 流计数器恒 0)")
|
||||
|
||||
|
||||
# ---- 元进度·MetaTable 表结构(元进度实现批 T1——行值全 provisional 归元进度设计批配读数;
|
||||
# 注入契约三字段必齐 = forge/conversion 覆盖恰三,Boss 两档 first ≥ repeat)----
|
||||
func _smoke_meta() -> void:
|
||||
print("--- 元进度·MetaTable 表结构 ---")
|
||||
var meta_tbl = _db.meta
|
||||
check(meta_tbl != null and meta_tbl.validate().is_empty(),
|
||||
"MetaTable 非空 + validate 零错(四行族词表封闭/唯一/连续/域界/first≥repeat/ratio 全过)")
|
||||
check(meta_tbl.forge_products.size() == 3 and meta_tbl.boss_keystone.size() == 3
|
||||
and meta_tbl.workshop_items.size() == 6 and meta_tbl.conversion.size() == 3,
|
||||
"MetaTable 四行族规模 3/3/6/3(forge/Boss/换算恰三 · 工坊恰六)")
|
||||
var attrs_ok := true
|
||||
for a in ["max_hp", "atk", "armor_class"]:
|
||||
attrs_ok = attrs_ok and not _db.meta_forge_row(a).is_empty() and not _db.meta_conversion_row(a).is_empty()
|
||||
check(attrs_ok, "forge/conversion attr 词表封闭恰全(注入契约三字段齐——meta_* 访问器可达)")
|
||||
var boss_ok := true
|
||||
for li in [1, 2, 3]:
|
||||
var brow: Dictionary = _db.meta_boss_keystone_row(li)
|
||||
boss_ok = boss_ok and not brow.is_empty() and int(brow.get("first_kill", 0)) >= int(brow.get("repeat_kill", -1))
|
||||
check(boss_ok, "boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)")
|
||||
_meta_pricing_and_buy()
|
||||
_meta_settle_and_workshop()
|
||||
_meta_injection()
|
||||
|
||||
|
||||
# 冒烟隔离存档名册(段首清陈旧 + 段尾清理——契约:测试不污染 user://save.json 真实档)
|
||||
const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry.json",
|
||||
"user://smoke_meta_settle.json", "user://smoke_meta_workshop.json",
|
||||
"user://smoke_meta_cap.json", "user://smoke_meta_white.json"]
|
||||
|
||||
|
||||
func _meta_smoke_cleanup() -> void:
|
||||
for p in _META_SMOKE_PATHS:
|
||||
for suffix in ["", ".tmp", ".corrupt.bak"]:
|
||||
if FileAccess.file_exists(p + suffix):
|
||||
DirAccess.remove_absolute(p + suffix)
|
||||
|
||||
|
||||
func _meta_new_save(path: String):
|
||||
var sv = load("res://core/save_manager.gd").new(path)
|
||||
sv.load_from_disk()
|
||||
return sv
|
||||
|
||||
|
||||
# ---- 元进度 ②几何定价 ③购买(余额充足持久 / 不足拒绝 / attr 出域拒绝)----
|
||||
func _meta_pricing_and_buy() -> void:
|
||||
print("--- 元进度·几何定价与购买(隔离档 smoke_meta_buy/dry)---")
|
||||
_meta_smoke_cleanup()
|
||||
var sv = _meta_new_save(_META_SMOKE_PATHS[0])
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
# ② 几何定价:tier 0/1/2 价格 == 迭代 floor(p × ratio/100) 复算值(tier 0 = base_price)且严格递增
|
||||
for attr in MetaTable.FORGE_ATTRS:
|
||||
var row: Dictionary = _db.meta_forge_row(attr)
|
||||
var base := int(row.get("base_price", 0))
|
||||
var ratio := int(row.get("price_ratio_pct", 100))
|
||||
var all_ok := true
|
||||
var prev := 0
|
||||
for tier in 3:
|
||||
sv.set_forge_level(attr, tier)
|
||||
var expect := base
|
||||
for _i in tier:
|
||||
expect = int(floor(float(expect) * float(ratio) / 100.0))
|
||||
all_ok = all_ok and mp.forge_price(attr) == expect and (tier == 0 or expect > prev)
|
||||
prev = expect
|
||||
check(all_ok, "%s 几何定价 tier 0/1/2 = 迭代 floor 复算且严格递增(base %d × ratio %d%%)" % [attr, base, ratio])
|
||||
sv.set_forge_level("atk", 0)
|
||||
# ③ 购买成功:级+1、余额−价;buy 内含写盘——reload 后持久
|
||||
sv.add_keystone(1000)
|
||||
var bal0: int = sv.keystone_balance()
|
||||
var lvl0: int = sv.forge_level("atk")
|
||||
var price0: int = mp.forge_price("atk")
|
||||
check(mp.buy_forge("atk") and sv.forge_level("atk") == lvl0 + 1 and sv.keystone_balance() == bal0 - price0,
|
||||
"购买成功(Lv %d→%d、余额 %d−%d)" % [lvl0, lvl0 + 1, bal0, price0])
|
||||
var sv_r = _meta_new_save(_META_SMOKE_PATHS[0])
|
||||
check(sv_r.forge_level("atk") == lvl0 + 1 and sv_r.keystone_balance() == bal0 - price0, "购买持久(reload 后级与余额不变)")
|
||||
# ③ 余额不足 / attr 出域:false 零副作用
|
||||
var sv_dry = _meta_new_save(_META_SMOKE_PATHS[1])
|
||||
var mp_dry = load("res://core/meta_progression.gd").new(_db, sv_dry)
|
||||
check(not mp_dry.buy_forge("atk") and sv_dry.keystone_balance() == 0 and sv_dry.forge_level("atk") == 0,
|
||||
"余额不足 false 零副作用(0 余额购买拒绝)")
|
||||
check(not mp_dry.buy_forge("luck") and sv_dry.keystone_balance() == 0, "attr 出域 false 零副作用(luck 不在词表)")
|
||||
|
||||
|
||||
# ---- 元进度 ④注入可见 + 白板成立(验收 4/2/3——程序面;MC 逐位回归归 T7 全量验证)----
|
||||
func _meta_injection() -> void:
|
||||
print("--- 元进度·注入可见与白板(隔离档 smoke_meta_white/dry)---")
|
||||
var sv = _meta_new_save(_META_SMOKE_PATHS[1])
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
sv.add_keystone(mp.forge_price("atk"))
|
||||
check(mp.buy_forge("atk") and int(mp.hero_override().get("atk", -1)) == int(_db.meta_forge_row("atk").get("flat_value", 0)),
|
||||
"买 atk 1 级 → hero_override().atk == 行 flat(验收4)")
|
||||
var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
|
||||
check(adr.start_adventure(920251, {"hero_override": mp.hero_override()}), "注入参数冒险启动(start_adventure params 通道)")
|
||||
var r = adr.runner()
|
||||
var ov: Dictionary = mp.hero_override()
|
||||
check(int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0)),
|
||||
"首战 hero_atk = 表值 + 永久强化 flat(begin_battle 同表达式——下一局能用到强化)")
|
||||
check(int(r.effective_max_hp()) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0))
|
||||
and int(adr.ledger["hero_hp"]) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)),
|
||||
"首战 hero_max_hp 与开局生命账本 = 表值 + 注入档")
|
||||
# 白板:同种子下 空参数 = 零强化参数 = 纯表值(逐位一致——注入通道默认值漏改在此转红)
|
||||
var adr_w = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
|
||||
check(adr_w.start_adventure(920251), "白板冒险启动(无 params 实参——B1 空参数路径)")
|
||||
var sv_white = _meta_new_save(_META_SMOKE_PATHS[5])
|
||||
var adr_z = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
|
||||
check(adr_z.start_adventure(920251, {"hero_override": load("res://core/meta_progression.gd").new(_db, sv_white).hero_override()}),
|
||||
"零强化参数冒险启动(新档 hero_override 全零)")
|
||||
var rw = adr_w.runner()
|
||||
var rz = adr_z.runner()
|
||||
check(int(rw.effective_atk()) == int(_db.hero.atk) and int(rw.effective_max_hp()) == int(_db.hero.max_hp)
|
||||
and int(rz.effective_atk()) == int(rw.effective_atk()) and int(rz.effective_max_hp()) == int(rw.effective_max_hp())
|
||||
and int(rz.effective_ac()) == int(rw.effective_ac()),
|
||||
"白板成立:空参数 = 零强化参数 = 纯表值(atk/max_hp/ac 三轴逐位一致——验收 2/3)")
|
||||
check(int(rw.effective_ac()) == int(_db.hero.armor_class) and int(r.effective_ac()) == int(_db.hero.armor_class),
|
||||
"ac 轴:白板恒表值、未换强化时注入零位移")
|
||||
_meta_smoke_cleanup()
|
||||
|
||||
|
||||
# ---- 元进度 ⑥Boss 两档 ⑦钥石不丢 ⑧工坊解锁 ⑩摘要副本与 cap(合成摘要驱动——settle 纯账面零跑局)----
|
||||
func _meta_settle_and_workshop() -> void:
|
||||
print("--- 元进度·收束入账与工坊(隔离档 smoke_meta_settle/workshop/cap)---")
|
||||
var sv = _meta_new_save(_META_SMOKE_PATHS[2])
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
var row1: Dictionary = _db.meta_boss_keystone_row(1)
|
||||
var row2: Dictionary = _db.meta_boss_keystone_row(2)
|
||||
# ⑥ 新档首通 L1:first = true、金额 = first_kill;total = big_reward + Σ drops;余额 = 累计入账
|
||||
var d1: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 3, "master_seed": 11})
|
||||
var drop1: Dictionary = d1["boss_drops"][0]
|
||||
check(bool(drop1.get("first", false)) and int(drop1.get("amount", 0)) == int(row1.get("first_kill", -1))
|
||||
and int(drop1.get("level", 0)) == 1, "新档首通 L1:boss_drops[0] {first:true, amount=first_kill}(验收5)")
|
||||
check(int(d1.get("total", 0)) == 3 + int(row1.get("first_kill", 0)), "明细 total = big_reward + Σ drops")
|
||||
check(sv.keystone_balance() == 3 + int(row1.get("first_kill", 0)), "余额 = 累计入账(大奖励 + Boss 两档)")
|
||||
# ⑥ 同档再通 → 重复杀档且恒定不递减(第三轮同额)
|
||||
var d2: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 3, "master_seed": 12})
|
||||
var drop2: Dictionary = d2["boss_drops"][0]
|
||||
check(not bool(drop2.get("first", false)) and int(drop2.get("amount", 0)) == int(row1.get("repeat_kill", -1)),
|
||||
"再通 L1:{first:false, amount=repeat_kill}(首杀标记置位后转小额)")
|
||||
var d3: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 3, "master_seed": 13})
|
||||
check(not bool(d3["boss_drops"][0].get("first", true)) and int(d3["boss_drops"][0].get("amount", 0)) == int(row1.get("repeat_kill", -1)),
|
||||
"重复杀第三轮恒定不递减(验收5——无衰减曲线)")
|
||||
# 混合归因:通关两关 → L1 重复 + L2 首杀
|
||||
var d4: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 2,
|
||||
"stopped_at": 0, "keystone_total": 4, "master_seed": 14})
|
||||
check(not bool(d4["boss_drops"][0].get("first", true)) and bool(d4["boss_drops"][1].get("first", false))
|
||||
and int(d4["boss_drops"][1].get("amount", 0)) == int(row2.get("first_kill", -1)),
|
||||
"两关通关逐关归因:L1 重复档 + L2 首杀档(first_kill 金额)")
|
||||
# ⑦ 败北零通关摘要:余额只增不减(零入账)、clear_counts 不变
|
||||
var bal_before: int = sv.keystone_balance()
|
||||
var cc1_before: int = sv.clear_count(1)
|
||||
var dz: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 0,
|
||||
"stopped_at": 1, "keystone_total": 0, "master_seed": 15})
|
||||
check(dz["boss_drops"].is_empty() and sv.keystone_balance() == bal_before and sv.clear_count(1) == cc1_before,
|
||||
"败北零通关摘要:零入账零标记(钥石只增不减——验收1;败北局 clear_counts 不变)")
|
||||
# ⑧ 工坊解锁:settle 内已随 refresh——总通关次数达阈值件数 == unlocked 数;重复 refresh 幂等
|
||||
var total_clears: int = sv.clear_count(1) + sv.clear_count(2) + sv.clear_count(3)
|
||||
var expect_n := 0
|
||||
for row in _db.meta_workshop_items():
|
||||
if total_clears >= int(row.get("clear_threshold", 99)):
|
||||
expect_n += 1
|
||||
var got_n := 0
|
||||
for st_row in mp.workshop_status():
|
||||
got_n += 1 if bool(st_row.get("unlocked", false)) else 0
|
||||
check(got_n == expect_n and mp.refresh_workshop_unlocks().is_empty(),
|
||||
"工坊解锁数 = 总通关 %d 次达阈值件数(%d 件)且重复 refresh 幂等" % [total_clears, expect_n])
|
||||
var sv_r = _meta_new_save(_META_SMOKE_PATHS[2])
|
||||
var r_n := 0
|
||||
for st_row in load("res://core/meta_progression.gd").new(_db, sv_r).workshop_status():
|
||||
r_n += 1 if bool(st_row.get("unlocked", false)) else 0
|
||||
check(r_n == expect_n, "工坊解锁持久(reload 后一致)")
|
||||
# ⑧ 干净档:通关 1 次 → 恰两件阈值 1 道具新解锁(new_unlocks 明细)
|
||||
var sv_w = _meta_new_save(_META_SMOKE_PATHS[3])
|
||||
var mp_w = load("res://core/meta_progression.gd").new(_db, sv_w)
|
||||
var u1: Dictionary = mp_w.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 0, "master_seed": 16})
|
||||
check(u1["new_unlocks"].size() == 2 and sv_w.item_unlocked("probe_dust") and sv_w.item_unlocked("sapper_order")
|
||||
and not sv_w.item_unlocked("loot_compass"), "通关 1 次 → 恰两件阈值 1 道具解锁(new_unlocks 明细正确)")
|
||||
# ⑩ recent_summaries:元素含 boss_drops 副本、最新在前、cap 10
|
||||
var rs: Array = sv.recent_summaries()
|
||||
check(rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops")
|
||||
and int(rs[0].get("master_seed", -1)) == 15, "摘要最新在前 + 元素含 boss_drops 副本(复盘可归因——验收5)")
|
||||
var sv_c = _meta_new_save(_META_SMOKE_PATHS[4])
|
||||
var mp_c = load("res://core/meta_progression.gd").new(_db, sv_c)
|
||||
for i in 12:
|
||||
mp_c.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 0,
|
||||
"stopped_at": 1, "keystone_total": 0, "master_seed": i})
|
||||
check(sv_c.recent_summaries().size() == 10 and int(sv_c.recent_summaries()[0].get("master_seed", -1)) == 11,
|
||||
"摘要 cap 10 且最新在前(12 连 settle 截断后 seed 11 居首)")
|
||||
_meta_smoke_cleanup()
|
||||
|
||||
|
||||
# ---- 战斗日志视图·行格式化呈现层同步闸(2026-09-20 真人试玩卡断 bug 回归:八型批伏击退役拆除
|
||||
# 伤害行 attach 字段后本视图拆行分支漏网未同步——敌方首笔攻击行带 baseline 无 attach,
|
||||
# _format_row Invalid access 使结算段中途夭折、界面卡死。行协议变更必须同步呈现层〔战斗结算 §行协议〕,
|
||||
|
||||
@@ -37,7 +37,53 @@ func _ready() -> void:
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
var t_ui_ready := Time.get_ticks_msec() - t0
|
||||
# 满配直达 Boss(基线在节点 2-4 就会败北——探针目标是 Boss 战的结算路径)
|
||||
# ---- 营地购买合成点击路径(元进度实现批——campbtn/购买/关闭全走真实按钮信号路径;
|
||||
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json);陈旧档段首清理)----
|
||||
for p0 in ["user://smoke_click_probe_camp.json", "user://smoke_click_probe_camp.json.tmp",
|
||||
"user://smoke_click_probe_reset.json", "user://smoke_click_probe_reset.json.tmp"]:
|
||||
if FileAccess.file_exists(p0):
|
||||
DirAccess.remove_absolute(p0)
|
||||
var iso_c = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp.json")
|
||||
iso_c.load_from_disk()
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c)
|
||||
var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地")
|
||||
if campbtn == null:
|
||||
_problems.append("营地入口按钮缺失(开始层 campbtn)")
|
||||
else:
|
||||
campbtn.pressed.emit() # 真实按钮信号路径 → 组合根 _open_camp
|
||||
await get_tree().process_frame
|
||||
if not main.camp_overlay.visible:
|
||||
_problems.append("营地层未打开(点击路径)")
|
||||
elif main.camp_overlay._forge_rows.is_empty():
|
||||
_problems.append("锻造柜台行缺失(点击路径)")
|
||||
else:
|
||||
var buy_btn: Button = main.camp_overlay._forge_rows[0].btn
|
||||
if not buy_btn.disabled:
|
||||
_problems.append("零余额锻造购买按钮未禁用(禁用态失守)")
|
||||
iso_c.add_keystone(9999)
|
||||
main.camp_overlay.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
if buy_btn.disabled:
|
||||
_problems.append("余额充足后购买按钮仍禁用(禁用态未随刷新)")
|
||||
var bal_before: int = iso_c.keystone_balance()
|
||||
var buy_attr: String = String(main.camp_overlay._forge_rows[0].attr)
|
||||
var price: int = main.meta.forge_price(buy_attr)
|
||||
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_buy → meta.buy_forge(写调用唯一归组合根)
|
||||
await get_tree().process_frame
|
||||
if int(iso_c.keystone_balance()) != bal_before - price:
|
||||
_problems.append("营地购买未出账(%d→%d 应 −%d)" % [bal_before, iso_c.keystone_balance(), price])
|
||||
var iso_chk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp.json")
|
||||
iso_chk.load_from_disk()
|
||||
if int(iso_chk.forge_level(buy_attr)) != 1:
|
||||
_problems.append("营地购买未持久(reload 后级 != 1)")
|
||||
main._on_camp_closed()
|
||||
await get_tree().process_frame
|
||||
if main.camp_overlay.visible or not main._start_overlay.visible:
|
||||
_problems.append("营地关闭后未恢复开始层")
|
||||
# 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底)
|
||||
var iso_reset = load("res://core/save_manager.gd").new("user://smoke_click_probe_reset.json")
|
||||
iso_reset.load_from_disk()
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_reset)
|
||||
main.tune_panel.set_sliders(200, 100)
|
||||
var t1 := Time.get_ticks_msec()
|
||||
main._start_full_level()
|
||||
@@ -272,3 +318,14 @@ func _dump_state() -> void:
|
||||
str(diag.battle_result), str(diag.settling)])
|
||||
print(" board_installed=%s facts_path='%s' refresh_used=%s flags=%s" % [
|
||||
str(diag.minesweeper_board_installed), str(diag.facts_path), str(diag.refresh_used_round), str(_flags_placed)])
|
||||
|
||||
|
||||
# 开始层按钮按文案前缀寻址(campbtn 句柄未存组合根成员——树遍历只读;与 ui_probe 同款助手)
|
||||
func _find_button_by_prefix(root: Node, prefix: String) -> Button:
|
||||
if root is Button and String(root.text).begins_with(prefix):
|
||||
return root
|
||||
for child in root.get_children():
|
||||
var found := _find_button_by_prefix(child, prefix)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
@@ -13,15 +13,54 @@ func _ready() -> void:
|
||||
var main = load("res://presentation/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
var problems: Array = []
|
||||
# ---- 放弃入口(2026-09-21 用户拍板——台账 119):默认隐藏(单关测试直入口无冒险可放弃)----
|
||||
if main.route_overlay.abandon_btn == null or main.route_overlay.abandon_btn.visible:
|
||||
print("[ui_probe] FAIL — 放弃按钮默认应隐藏(单关测试直入口关)")
|
||||
get_tree().quit(1)
|
||||
return
|
||||
# ---- 营地覆盖层(元进度实现批——主菜单入口/开合/余额文案/购买禁用态;
|
||||
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json——SaveManager 路径注入纪律)----
|
||||
var iso = load("res://core/save_manager.gd").new("user://smoke_ui_probe_camp.json")
|
||||
iso.load_from_disk()
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso)
|
||||
var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地")
|
||||
if campbtn == null:
|
||||
problems.append("营地入口按钮缺失(开始层 campbtn)")
|
||||
else:
|
||||
campbtn.pressed.emit() # 合成信号路径(真实按钮 → 组合根 _open_camp)
|
||||
await get_tree().process_frame
|
||||
if not main.camp_overlay.visible:
|
||||
problems.append("营地层未打开(campbtn 路径)")
|
||||
elif main._start_overlay.visible:
|
||||
problems.append("营地层打开时开始层未隐藏(规范 2.3 遮挡纪律)")
|
||||
else:
|
||||
if not String(main.camp_overlay._balance_label.text).begins_with("钥石"):
|
||||
problems.append("营地余额文案缺失(%s)" % main.camp_overlay._balance_label.text)
|
||||
if main.camp_overlay._forge_rows.size() != 3:
|
||||
problems.append("锻造柜台行数错误(%d ≠ 3)" % main.camp_overlay._forge_rows.size())
|
||||
else:
|
||||
var zero_disabled := true
|
||||
for card in main.camp_overlay._forge_rows:
|
||||
zero_disabled = zero_disabled and bool(card.btn.disabled) # 零余额全禁用
|
||||
if not zero_disabled:
|
||||
problems.append("零余额下锻造购买按钮未全禁用(余额不足禁用态失守)")
|
||||
if main.camp_overlay._workshop_rows.size() != 6:
|
||||
problems.append("工坊柜台行数错误(%d ≠ 6)" % main.camp_overlay._workshop_rows.size())
|
||||
main._on_camp_closed()
|
||||
await get_tree().process_frame
|
||||
if main.camp_overlay.visible or not main._start_overlay.visible:
|
||||
problems.append("营地关闭后未恢复开始层(开合往返失守)")
|
||||
# 换回全新零档(营地段购买态不泄入后续冒险段——确定性;先删后建防陈旧档泄入)
|
||||
for p in ["user://smoke_ui_probe_camp.json", "user://smoke_ui_probe_camp.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
var iso_zero = load("res://core/save_manager.gd").new("user://smoke_ui_probe_camp.json")
|
||||
iso_zero.load_from_disk()
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_zero)
|
||||
main._start_full_level()
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var problems: Array = []
|
||||
# ---- 放弃入口·冒险模式可见 + 战斗中按下被拒(真实按钮路径——入口挂选路停点,战斗中拒绝为防御分支)----
|
||||
if main.route_overlay.abandon_btn == null or not main.route_overlay.abandon_btn.visible:
|
||||
problems.append("冒险模式放弃按钮未开放(选路停点入口在案)")
|
||||
@@ -154,3 +193,14 @@ func _ready() -> void:
|
||||
push_error("[ui_probe] FAIL — " + p)
|
||||
print("[ui_probe] FAIL — " + "; ".join(problems))
|
||||
get_tree().quit(1)
|
||||
|
||||
|
||||
# 开始层按钮按文案前缀寻址(campbtn 句柄未存组合根成员——树遍历只读;text 前缀匹配首枚按钮)
|
||||
func _find_button_by_prefix(root: Node, prefix: String) -> Button:
|
||||
if root is Button and String(root.text).begins_with(prefix):
|
||||
return root
|
||||
for child in root.get_children():
|
||||
var found := _find_button_by_prefix(child, prefix)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
Reference in new issue
Block a user