# VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2) # Date: 2026-09-10 # ConfigDB autoload(数据配置)——数值表单点访问入口(Rule 2)+ 校验双通道(Rule 6)+ R1 行解析。 # 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚,命名规范见 CLAUDE.md) # 切片注:headless --script 模式下由冒烟脚本显式实例化(数据配置 AC 前言);玩法代码禁止绕过本单点自建 load()。 class_name ConfigDb extends Node const SCHEMA_VERSION := 1 const TABLE_MANIFEST := { "action_economy": "action_economy.tres", "variation": "variation.tres", "status_effects": "status_effects.tres", "board_gen": "board_gen.tres", "enemy": "enemy.tres", "skill": "skills.tres", "hero": "hero.tres", "reward": "reward.tres", "settlement": "settlement.tres", "level": "level.tres", "difficulty": "difficulty.tres", "shop": "shop.tres", # GP+商店房实施批 2026-09-21(表与 schema 落籍归并行数据面——本批只接线加载与消费) "meta": "meta.tres", # 元进度实现批 2026-09-21(MetaTable 四行族——schema 与表同批落籍) "meta_tree": "meta_tree.tres", # 迭代 12 T8 双层 BD 天赋树(MetaTreeTable 定义表 Rule 4 豁免——R1 甲融合案 33 节点) "aux_items": "aux_items.tres", # 迭代 7 T1 魔法道具内容批(AuxItemTable 六行——定义表 Rule 4 豁免;罗盘 retired/失义件 deferred 标记见 schema 头注) "adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点) "attunement_growth": "attunement_growth.tres", # 迭代 11 第①批 同调实施批 T1(AttunementGrowthTable 单行全局——同调位里程碑谓词表;单行先例 Rule 4 豁免) "relics": "relics.tres", # 迭代 12 T1 双层 BD 机制件基建(RelicTable 定义表 Rule 4 豁免——AuxItemTable 先例;空表合法态=主时间线零扰动) } var action_economy: ActionEconomyTable var variation: VariationTable var status_effects: StatusEffectTable var board_gen: BoardGenTable var enemy: EnemyTable var skill: SkillTable var hero: HeroTable var reward: RewardTable var settlement: SettlementTable var level: LevelTable var difficulty: DifficultyTable # GP+商店房实施批(2026-09-21):ShopTable 类型化持用(schema class_name 已落籍——code-review 2026-09-29 P2-4 收口; # 消费面一律走下方 shop_* 访问器,禁直读行集) var shop: ShopTable = null # 元进度实现批(2026-09-21):MetaTable 类型化持用(schema 与表同批落籍,无 shop 先例的落籍间隙问题) var meta: MetaTable var meta_tree: MetaTreeTable # 迭代 12 T8 类型化持用(消费面走 tree_node 访问器) var aux_items: AuxItemTable # 迭代 7 T1 类型化持用(消费面走 aux_* 访问器) var adventure: AdventureTable var attunement_growth: AttunementGrowthTable # 迭代 11 第①批 T1 类型化持用(消费面走 attunement_* 访问器) var relics: RelicTable # 迭代 12 T1 类型化持用(消费面走 relic_* 访问器) var is_ready := false var _diagnostics: Array[Dictionary] = [] # {severity, category, message} var _resolver: ConfigDbResolver # R1 难度行解析协作类(2026-10-02 修复批 T5 拆分——门面公有签名零变化) var _validator := ConfigDbCrossValidator.new() # 跨表校验协作类(无状态——声明期即建) var _reload_gate := false # 数据配置 EC8——战斗结算 唯一调用方 func _init() -> void: _resolver = ConfigDbResolver.new(self) # 协作类持门面引用(诊断/致命通道回呼) func push_diag(severity: String, category: String, message: String) -> void: _diagnostics.append({"severity": severity, "category": category, "message": message}) if severity == "warning": push_warning("[ConfigDB/%s] %s" % [category, message]) else: push_error("[ConfigDB/%s] %s" % [category, message]) # 运行期数据致命(2026-09-18 用户拍板:越域记 Fatal 级日志后立即退出——数据配置通道 fail-fast 纪律补齐到查询期) func _fatal(category: String, message: String) -> void: push_diag("fatal", category, message) push_error("[ConfigDB/%s] FATAL: %s —— 数据配置致命,进程退出" % [category, message]) var main_loop := Engine.get_main_loop() if main_loop is SceneTree: main_loop.quit(1) # 帧末退出,退出码 1——调用方须立即 return,勿再消费返回值 else: OS.crash("ConfigDB/%s fatal: %s" % [category, message]) func reset_diagnostics() -> void: _diagnostics.clear() _resolver.reset_warn_dedup() # R1 告警去重注册表(协作类持钥——清空时点与拆分前一致) func diagnostics() -> Array: return _diagnostics.duplicate() func diagnostics_count(severity: String) -> int: var n := 0 for d in _diagnostics: if d.severity == severity: n += 1 return n func _ready() -> void: load_tables("res://data/tables", false) func load_tables(root: String, collect_only: bool) -> bool: is_ready = false _diagnostics.clear() _resolver.reset_warn_dedup() var loaded := {} for key in TABLE_MANIFEST.keys(): var path := root.path_join(TABLE_MANIFEST[key]) if not ResourceLoader.exists(path): push_diag("error", "missing_file", "表文件缺失: %s" % path) continue var res: Resource = load(path) if res == null: push_diag("error", "load_failed", "表加载失败: %s" % path) continue loaded[key] = res for e in res.validate(): push_diag("error", e.category, "%s → %s" % [key, e.message]) # 跨表校验(悬空引用 / 池护栏——全部加载期致命) _cross_validate(loaded) var has_errors := diagnostics_count("error") > 0 if has_errors and not collect_only: # 游戏通道:致命(Rule 6)——is_ready 恒 false,调用方(主场景)中止构建 return false action_economy = loaded.get("action_economy") variation = loaded.get("variation") status_effects = loaded.get("status_effects") board_gen = loaded.get("board_gen") enemy = loaded.get("enemy") skill = loaded.get("skill") hero = loaded.get("hero") reward = loaded.get("reward") settlement = loaded.get("settlement") level = loaded.get("level") difficulty = loaded.get("difficulty") shop = loaded.get("shop") meta = loaded.get("meta") aux_items = loaded.get("aux_items") adventure = loaded.get("adventure") attunement_growth = loaded.get("attunement_growth") relics = loaded.get("relics") meta_tree = loaded.get("meta_tree") is_ready = not has_errors return not has_errors # ---- relic 访问器(迭代 12 T1——消费面一律走本组,禁直读行集)---- func relic_row(relic_id: String) -> Dictionary: return relics.row_by_id(relic_id) if relics != null else {} # 机制件带令牌构建源(池接线——adventure_runner._build_bonus_deck 消费;行 rarity→投放档映射) func relic_rows() -> Array: return relics.rows if relics != null else [] # 天赋树节点行(迭代 12 T8——MetaProgression 树段/营地树 UI 取数口) func tree_node(node_id: String) -> Dictionary: return meta_tree.row_by_id(node_id) if meta_tree != null else {} func tree_rows() -> Array: return meta_tree.rows if meta_tree != null else [] # 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体自 2026-10-02 修复批迁出: # ConfigDbCrossValidator(core/config_db_cross_validator.gd)承载,门面公有签名零变化。 func _cross_validate(loaded: Dictionary) -> void: _validator.cross_validate(loaded, push_diag) # 池模板 × 技能行跨表校验(冒烟直验独立入口——签名与消费面零变化,实现归协作类) func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array: return _validator.validate_skill_pool_links(skill_table, enemy_table, status_table) # ---- R1 难度行解析(2026-09-18 用户拍板修订:ceil 向上取整保证偏难;域越界与内容缺口 Fatal 退出)---- # 解析本体自 2026-10-02 修复批迁出:ConfigDbResolver(core/config_db_resolver.gd)承载—— # 含 _resolve_progress/_ceil_key 与 R1 告警去重注册表;门面同名方法委托,越域 Fatal 语义逐位一致。 func resolve_board_gen(L: int, D: int) -> Dictionary: return _resolver.resolve_board_gen(L, D) func resolve_enemy_rowset(L: int, D: int) -> Dictionary: return _resolver.resolve_enemy_rowset(L, D) # 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故) func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary: return _resolver.resolve_difficulty_modifiers(L, D) # ---- 地形变异 variant_pool_gate:候选池门控(∅ board_types = 通配;2026-09-19 用户裁定字段名对齐 # board_type 词表——biome_tags 系旧观念残留退役)---- func variant_pool(level_index: int, board_type: String) -> Array: var pool: Array[Dictionary] = [] for row in variation.rows: if int(row.get("unlock_level", 999)) > level_index: continue var tags: Array = row.get("board_types", []) if tags.is_empty(): pool.append(row) continue if tags.has(board_type): pool.append(row) return pool func status_effect_row(effect_id: String) -> Dictionary: return status_effects.row_by_id(effect_id) # ---- 关卡行集访问器(2026-10-02 修复批 T5 表访问收口——level_runner/meta_progression/adventure_runner # 直扫 db.level.rows 退役;消费面一律走本访问器,行集解析单点归 LevelTable)---- # 关卡行单点读取(键 = level_index,缺行返回空字典——消费方按既有空行防御路径处理) func level_row(level_index: int) -> Dictionary: return level.row_by_level_index(level_index) if level != null else {} # 序列总关数(AdventureRunner levels_total / MetaProgression 通关累加域的取数口径) func level_count() -> int: return level.rows.size() if level != null else 0 # ---- GP+商店房实施批(2026-09-21):ShopTable 消费面(行集解析单点——runner/战斗编排禁自扫表)---- # ── 魔法道具访问器(迭代 7 T1——消费面一律走访问器,禁直读行集)── # 活行集(非 retired 非 deferred)——投放校验/奖励节点池/呈现可用性的唯一取数口径 func aux_active_rows() -> Array: return aux_items.active_rows() if aux_items != null else [] func aux_item_row(item_id: String) -> Dictionary: return aux_items.row_by_id(item_id) if aux_items != null else {} # 过渡渠道活行(form 空 = 切片期过渡形态)——掉落投放/停点池/工坊解锁取数口径;同调行(form 非空) # 永不入过渡渠道(迭代 11 第①批 T1;T7 退役面切换后本口径随渠道消亡) func aux_transitional_rows() -> Array: return aux_items.transitional_rows() if aux_items != null else [] # 同调行集(form 非空——装配覆盖层/效果接线的取数口径,迭代 11 第①批 T1/T4) func aux_attunement_rows() -> Array: return aux_items.attunement_rows() if aux_items != null else [] # 活行词表(掉落投放 effect_id 白名单——过渡渠道口径) func aux_live_ids() -> Dictionary: var out := {} for row in aux_transitional_rows(): out[String(row.get("item_id", ""))] = true return out # ── 同调轴访问器(迭代 11 第①批 T1——消费面一律走访问器,禁直读行集)── func attunement_slot_cap() -> int: return attunement_growth.slot_cap if attunement_growth != null else 0 func attunement_milestones() -> Array: return attunement_growth.milestones if attunement_growth != null else [] # 行族四轴(数据契约,行集归 shop.tres):gp_drops {tier, mean, roll_min, roll_max} · # level_gp_bonus {level_index, bonus} · products {product_id, display_name, attr, flat_value, # base_price, price_increment} · price_level_bonus {level_index, bonus}。 # 行族经 ShopTable 同名数组属性读取(.get 容缺——表落籍前查询恒空,掉落按 0 兑付的兜底在调用方)。 func shop_gp_drop_row(tier: String) -> Dictionary: for row in _shop_rows("gp_drops"): if String(row.get("tier", "")) == tier: return row return {} func shop_level_gp_bonus(level_index: int) -> int: for row in _shop_rows("level_gp_bonus"): if int(row.get("level_index", -1)) == level_index: return int(row.get("bonus", 0)) return 0 func shop_price_level_bonus(level_index: int) -> int: for row in _shop_rows("price_level_bonus"): if int(row.get("level_index", -1)) == level_index: return int(row.get("bonus", 0)) return 0 func shop_products() -> Array: return _shop_rows("products") func shop_product_row(product_id: String) -> Dictionary: for row in _shop_rows("products"): if String(row.get("product_id", "")) == product_id: return row return {} # ── 共享池货架访问器(M-4 迭代 11 第②批 T1——消费面一律走访问器,禁直读行集)── # 货架行族({kind_id, display_name, base_price}——行序即抽签/呈现稳定序) func shop_pool_rows() -> Array: return _shop_rows("pool_shelf") func shop_pool_row(kind_id: String) -> Dictionary: for row in _shop_rows("pool_shelf"): if String(row.get("kind_id", "")) == kind_id: return row return {} # 乘法价格因子(provisional 投值——0 = 表未投值,消费方按 fail-fast 域校验拦截) func shop_price_a() -> float: return float(shop.price_a) if shop != null else 0.0 func shop_price_b() -> float: return float(shop.price_b) if shop != null else 0.0 # 行族数组读取(ShopTable 属性名 = 行族名;表未落籍/属性缺失 = 空集非错误态) func _shop_rows(family: String) -> Array: if shop == null: return [] var arr = shop.get(family) 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 {} # Y 刷新升级行单点读取(迭代 11 第①批 T4——键 = 等级,缺行返回空字典=未购/已满级合法态) func meta_attune_refresh_row(level: int) -> Dictionary: for row in _meta_rows("attune_refresh_upgrade"): if int(row.get("level", -1)) == level: 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) # ---- 数据配置 EC8:debug 热重载门(战斗结算 唯一调用方;切片实现 = 门标志 + 拒绝语义)---- func set_reload_gate(gated: bool) -> void: _reload_gate = gated func reload() -> bool: if _reload_gate: push_diag("warning", "reload_gated", "战斗中热重载被拒绝(数据配置 EC8)") return false var ok := load_tables("res://data/tables", false) if ok: push_diag("debug", "config_reloaded", "表热重载完成") return ok