# 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 与表同批落籍) "aux_items": "aux_items.tres", # 迭代 7 T1 魔法道具内容批(AuxItemTable 六行——定义表 Rule 4 豁免;罗盘 retired/失义件 deferred 标记见 schema 头注) "adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点) } 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 aux_items: AuxItemTable # 迭代 7 T1 类型化持用(消费面走 aux_* 访问器) var adventure: AdventureTable var is_ready := false var _diagnostics: Array[Dictionary] = [] # {severity, category, message} var _warn_dedup: Dictionary[String, bool] = {} # R1 四元组去重注册表 var _reload_gate := false # 数据配置 EC8——战斗结算 唯一调用方 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() _warn_dedup.clear() 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() _warn_dedup.clear() 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") is_ready = not has_errors return not has_errors func _cross_validate(loaded: Dictionary) -> void: var st: StatusEffectTable = loaded.get("status_effects") var rt: RewardTable = loaded.get("reward") var st_tab: SettlementTable = loaded.get("settlement") if st == null or rt == null or st_tab == null: return # 单表校验已报错,不连锁 var effect_ids := {} for row in st.rows: effect_ids[row.get("effect_id", "")] = true # ① RewardTable effect_id 悬空(percent 行引用 StatusEffectTable——悬空加载致命)。 # GP+商店房实施批(2026-09-21)例外:gp_gain/gp_loss = GP 战斗内缓冲(escrow)通道的路由键 # (战斗结算 _apply_gp_row 兑付——不入增益状态容器),词表封闭按字面豁免悬空引用校验。 for row in rt.rows: if String(row.get("cell_type", "")) == RewardTable.AUX_ITEM_TYPE: continue # aux 行 effect_id=item_id——归 ①b 双表校验(reward_dangling_aux),不入增益词表面 var eff: String = row.get("effect_id", "") if eff != "" and eff != "gp_gain" and eff != "gp_loss" and not effect_ids.has(eff): push_diag("error", "dangling_effect", "RewardTable %s 引用不存在的 effect_id: %s" % [row.get("reward_id"), eff]) # ①b RewardTable aux 投放行双表校验(迭代 7 T1——reward_dangling_aux):aux 行 effect_id = item_id, # 须命中 AuxItemTable 活行(非 retired 非 deferred——罗盘退役/失义件不投放)。retired 行被引用=退役 # 后仍投放(连带错误),同拦。 var aux_live := {} var aux_tab = loaded.get("aux_items") if aux_tab != null: for arow in aux_tab.active_rows(): aux_live[String(arow.get("item_id", ""))] = true for row in rt.rows: if String(row.get("cell_type", "")) != RewardTable.AUX_ITEM_TYPE: continue var aux_eff: String = String(row.get("effect_id", "")) if aux_eff == "": # 负占位行(雷格极性字典兜底,零兑付)——不入投放词表校验 continue if not aux_live.has(aux_eff): push_diag("error", "reward_dangling_aux", "RewardTable %s aux 行引用无效道具 %s(retired/deferred/不存在): %s" % [row.get("reward_id"), aux_eff, "双表校验"]) # ①c F2 投放维度覆盖(魔法道具 GDD——五维度各 ≥1 掉落权重为正的活行):豁免面 {plan, info, counter} # = 罗盘退役(plan,2026-09-29 拍板①乙)+显影件 Alpha 重定(info/counter 悬置)——豁免随 Alpha 重定批消减 if aux_tab != null: var covered := {} for row in rt.rows: if String(row.get("cell_type", "")) != RewardTable.AUX_ITEM_TYPE or int(row.get("weight", 0)) <= 0: continue var arow: Dictionary = aux_tab.row_by_id(String(row.get("effect_id", ""))) for tag in arow.get("dimension_tags", []): covered[String(tag)] = true var f2_exempt := {"plan": true, "info": true, "counter": true} # 豁免面(随 Alpha 重定/天赋批消减) for d in AuxItemTable.DIMENSIONS: if f2_exempt.has(d): continue if not covered.has(d): push_diag("error", "dimension_coverage", "F2 投放维度覆盖缺 %s(豁免面外零活行投放)" % d) # ② SettlementTable.effect_pool_map 悬空 for eff in st_tab.effect_pool_map.keys(): if not effect_ids.has(eff): push_diag("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff) # ③ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {enemy_action, hero_attack, flag_miss}——2026-09-15 格面资源批删 item 键) var required_sources := ["safe_cell", "flag_hit", "mine_hit", "clear_reward", "hero_attack", "enemy_action", "flag_miss"] for s in required_sources: if not st_tab.source_label_map.has(s): push_diag("error", "segorder_violation", "source_label_map 缺必需键: %s" % s) # ④ LevelTable 节点悬空引用(battle/boss 的 (L,node_depth) 须在 board_gen 与 enemy 两表有精确行—— # 线性推进器按精确深度行跑,floor 兜底静默降难度属调参事故,加载期直接拦) var lv: LevelTable = loaded.get("level") var bg: BoardGenTable = loaded.get("board_gen") var en: EnemyTable = loaded.get("enemy") if lv != null and bg != null and en != null: var bg_keys := {} for row in bg.rows: bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true var en_keys := {} for row in en.rows: en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true for lrow in lv.rows: var li: int = int(lrow.level_index) # 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes) var node_lists: Array[Array] = [] var tpls: Array = lrow.get("templates", []) if tpls.is_empty(): node_lists.append(lrow.get("nodes", [])) else: for tpl in tpls: node_lists.append(tpl.get("nodes", [])) for nodes_arr in node_lists: for node in nodes_arr: var t: String = String(node.get("type", "")) if t != "battle" and t != "boss": continue var key := "%d_%d" % [li, int(node.get("node_depth", -1))] if not bg_keys.has(key): push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key]) if not en_keys.has(key): push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key]) # ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验) var skt = loaded.get("skill") if skt != null and en != null: for e in validate_skill_pool_links(skt, en, loaded.get("status_effects")): push_diag("error", e.category, e.message) # 池模板 × 技能行跨表校验(加载期致命;返回错误列表——ConfigDB 诊断通道与冒烟直验共用): # skill_id 悬空致命 + 池构成纪律(每池恰一行普攻、增益技 ≤1、妨害技 ≤1——proposal §2.1 结构保证 + # proposal-status-vocabulary §1.2)+ 施放类 side 守卫(增益引用 enemy 侧 / 妨害引用 hero 侧状态行)。 # status_table 显式入参:装载序上本校验先于成员赋值执行(self.status_effects 尚为 Nil)——装载通道传 loaded 表, # 冒烟直验传 _db.status_effects;传 null 时 side 守卫跳过(其他校验不受影响)。 func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array: var errs: Array[Dictionary] = [] var skill_types := {} for row in skill_table.rows: skill_types[String(row.get("skill_id", ""))] = String(row.get("type", "")) # 施放类 side 守卫(新效应词汇批 2026-09-19):buff → 敌方侧状态 / debuff → 主角侧状态,防反向挂接 for row in skill_table.rows: var st: String = String(row.get("type", "")) if st != "buff" and st != "debuff": continue var want_side: String = "enemy" if st == "buff" else "hero" var eff: String = String(row.get("buff_ref", "")) var srow: Dictionary = status_table.row_by_id(eff) if status_table != null else {} if not srow.is_empty() and String(srow.get("target_side", "")) != want_side: errs.append({"category": "cast_side", "message": "SkillTable %s(%s)引用 %s 的 target_side=%s ≠ %s(施放类 side 守卫)" % [String(row.get("skill_id", "")), st, eff, String(srow.get("target_side", "")), want_side]}) for row in enemy_table.rows: var eid: String = String(row.get("enemy_id", "")) var basic_n := 0 var buff_n := 0 var debuff_n := 0 for entry in row.get("pool", []): var sid: String = String(entry.get("skill_id", "")) if not skill_types.has(sid): errs.append({"category": "dangling_skill", "message": "EnemyTable %s 池引用不存在的 skill_id: %s(悬空致命)" % [eid, sid]}) continue match skill_types[sid]: "basic": basic_n += 1 "buff": buff_n += 1 "debuff": debuff_n += 1 if basic_n != 1: errs.append({"category": "pool_composition", "message": "EnemyTable %s 池普攻行数 ≠ 1 (%d)——普攻冷却恒 0 = 空池不可能的结构前提" % [eid, basic_n]}) if buff_n > 1: errs.append({"category": "pool_composition", "message": "EnemyTable %s 池增益技 > 1 (%d)——每池增益 ≤1 纪律(纠偏三)" % [eid, buff_n]}) if debuff_n > 1: errs.append({"category": "pool_composition", "message": "EnemyTable %s 池妨害技 > 1 (%d)——每池妨害 ≤1 纪律(新效应词汇批 2026-09-19)" % [eid, debuff_n]}) return errs # ---- R1 难度行解析(2026-09-18 用户拍板修订:ceil 向上取整保证偏难;域越界与内容缺口 Fatal 退出)---- func resolve_board_gen(L: int, D: int) -> Dictionary: return _resolve_progress(board_gen.rows, L, D, "board_gen") func resolve_enemy_rowset(L: int, D: int) -> Dictionary: return _resolve_progress(enemy.rows, L, D, "enemy") # 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故) func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary: if difficulty == null: return {} for row in difficulty.rows: if int(row.get("level_index", 0)) == L and int(row.get("node_depth", -1)) == D: return { "hp_mult": clampf(float(row.get("hp_mult", 1.0)), 0.1, 10.0), "atk_mult": clampf(float(row.get("atk_mult", 1.0)), 0.1, 10.0), } return {} func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Dictionary: if L < 1 or D < 0: _fatal("request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D]) return {} var level_keys: Array[int] = [] for row in rows: var lv := int(row.get("level_index", 0)) if not level_keys.has(lv): level_keys.append(lv) if level_keys.is_empty(): _fatal("structural", "%s 行集为空——锚行校验应已拦截" % table_id) return {} level_keys.sort() var l_star := _ceil_key(level_keys, L) var l_clamped := l_star < 0 if l_clamped: l_star = int(level_keys[-1]) # 超出最大行 → 取最大行封顶(2026-09-18 追加拍板:可取范围内向上取整,超出用最大值) var depth_keys: Array[int] = [] for row in rows: if int(row.get("level_index", 0)) == l_star: var dv := int(row.get("node_depth", 0)) if not depth_keys.has(dv): depth_keys.append(dv) depth_keys.sort() var d_star := _ceil_key(depth_keys, D) var d_clamped := d_star < 0 if d_clamped: d_star = int(depth_keys[-1]) var missing := "" if l_clamped and d_clamped: missing = "both_clamp" elif l_clamped: missing = "level_clamp" elif d_clamped: missing = "depth_clamp" elif l_star != L and d_star != D: missing = "both_ceil" elif l_star != L: missing = "level_ceil" elif d_star != D: missing = "depth_ceil" if missing != "": var dedup_key := "%s|%s|%d|%d" % [table_id, missing, l_star, d_star] if not _warn_dedup.has(dedup_key): _warn_dedup[dedup_key] = true var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)" push_diag("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing]) for row in rows: if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star: return row return {} # 取 sorted_keys 中第一个 >= request 的键;无(请求高于最大行)返回 -1 → 调用方 Fatal func _ceil_key(sorted_keys: Array, request: int) -> int: for k in sorted_keys: if int(k) >= request: return int(k) return -1 # ---- 地形变异 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) # ---- 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 {} # 活行词表(掉落投放 effect_id 白名单) func aux_live_ids() -> Dictionary: var out := {} for row in aux_active_rows(): out[String(row.get("item_id", ""))] = true return out # 行族四轴(数据契约,行集归 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 {} # 行族数组读取(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 {} # 行族数组读取(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