424 lines
17 KiB
GDScript
424 lines
17 KiB
GDScript
# 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 节点)
|
||
"treasure_items": "treasure_items.tres", # 迭代 7 T1 秘宝内容批(TreasureItemTable 六行——定义表 Rule 4 豁免;罗盘 retired/失义件 deferred 标记见 schema 头注)
|
||
"adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点)
|
||
"attunement_growth": "attunement_growth.tres", # 迭代 11 第①批 同调实施批 T1(AttunementGrowthTable 单行全局——同调位里程碑谓词表;单行先例 Rule 4 豁免)
|
||
"feats": "feats.tres", # 迭代 12 T1 双层 BD 规则改写型特性基建(FeatTable 定义表 Rule 4 豁免——TreasureItemTable 先例;空表合法态=主时间线零扰动)
|
||
}
|
||
|
||
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 treasure_items: TreasureItemTable # 迭代 7 T1 类型化持用(消费面走 treasure_* 访问器)
|
||
var adventure: AdventureTable
|
||
var attunement_growth: AttunementGrowthTable # 迭代 11 第①批 T1 类型化持用(消费面走 attunement_* 访问器)
|
||
var feats: FeatTable # 迭代 12 T1 类型化持用(消费面走 feat_* 访问器)
|
||
|
||
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")
|
||
treasure_items = loaded.get("treasure_items")
|
||
adventure = loaded.get("adventure")
|
||
attunement_growth = loaded.get("attunement_growth")
|
||
feats = loaded.get("feats")
|
||
meta_tree = loaded.get("meta_tree")
|
||
is_ready = not has_errors
|
||
return not has_errors
|
||
|
||
|
||
# ---- relic 访问器(迭代 12 T1——消费面一律走本组,禁直读行集)----
|
||
|
||
func feat_row(feat_id: String) -> Dictionary:
|
||
return feats.row_by_id(feat_id) if feats != null else {}
|
||
|
||
|
||
# 规则改写型特性带令牌构建源(池接线——adventure_runner._build_curio_deck 消费;行 rarity→投放档映射)
|
||
func feat_rows() -> Array:
|
||
return feats.rows if feats != 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 treasure_active_rows() -> Array:
|
||
return treasure_items.active_rows() if treasure_items != null else []
|
||
|
||
|
||
func treasure_item_row(item_id: String) -> Dictionary:
|
||
return treasure_items.row_by_id(item_id) if treasure_items != null else {}
|
||
|
||
|
||
# 过渡渠道活行(form 空 = 切片期过渡形态)——掉落投放/停点池/工坊解锁取数口径;同调行(form 非空)
|
||
# 永不入过渡渠道(迭代 11 第①批 T1;T7 退役面切换后本口径随渠道消亡)
|
||
func treasure_transitional_rows() -> Array:
|
||
return treasure_items.transitional_rows() if treasure_items != null else []
|
||
|
||
|
||
# 同调行集(form 非空——装配覆盖层/效果接线的取数口径,迭代 11 第①批 T1/T4)
|
||
func treasure_attunement_rows() -> Array:
|
||
return treasure_items.attunement_rows() if treasure_items != null else []
|
||
|
||
|
||
# 活行词表(掉落投放 effect_id 白名单——过渡渠道口径)
|
||
func treasure_live_ids() -> Dictionary:
|
||
var out := {}
|
||
for row in treasure_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
|