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

326 lines
13 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 与表同批落籍)
"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 _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")
is_ready = not has_errors
return not has_errors
# 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体自 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 {}
# 活行词表(掉落投放 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