Files
gplan-2/game/core/config_db.gd
T

424 lines
17 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 系旧观念残留退役)----
# 门控第三维 = 节点身份(全局作用域变异首发批 2026-10-04):scope=global 行仅 boss/elite 节点入池,
# 其余节点(battle 等)静默跳过——门控兜底 P0 口径(零报错零警告);elite 系前向词位(首发零表行)。
# node_type 缺省 "" = 旧双参调用口径(global 行跳过)——既有测试/冒烟调用零扰动。
func variant_pool(level_index: int, board_type: String, node_type: String = "") -> Array:
var pool: Array[Dictionary] = []
for row in variation.rows:
if String(row.get("scope", "region")) == "global" \
and node_type != "boss" and node_type != "elite":
continue
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
# 全局型单行决议(战斗编排装板期消费——访问器纪律:消费面禁直扫表):
# boss/elite 节点取候选池内首个 scope=global 行(首发批全局池恒恰一行;多行为配置越界态,
# 按表序取首行确定性兜底);空 = 无全局型(调用方回落 region 布置——兜底回落合法态)
func global_variant_row(level_index: int, board_type: String, node_type: String) -> Dictionary:
if node_type != "boss" and node_type != "elite":
return {}
for row in variant_pool(level_index, board_type, node_type):
if String(row.get("scope", "region")) == "global":
return row
return {}
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