工程(图鉴工具·采集端与渲染端·用户令): compendium_dump经ConfigDB导出enemy+skills全行JSON(out/tables_root参数化)+compendium_gen生成自包含HTML图鉴(按关按带分组·池份额·技能语义); 对账三件过+悬空引用fail-fast非零退出验证过+结构审计PASS
This commit is contained in:
1 parent
e1d938eff2
commit
06d915d6cc
3 files changed
+357
No files matched your search
@@ -0,0 +1,108 @@
|
||||
## 怪物图鉴外置工具·采集端:经 ConfigDB 单点读 enemy + skills 两表全行,行数据原样导出 JSON
|
||||
## 到 sim-data/compendium/dump.json(渲染端 tools/compendium_gen.py 消费,生成自包含 HTML 图鉴)。
|
||||
## 运行:Godot_console.exe --headless --path . --script res://sim/compendium_dump.gd -- [out=<绝对路径>] [tables_root=<res:// 路径>]
|
||||
## out 缺省 = 仓库根 sim-data/compendium/dump.json(res:// 的上一级);目录不存在则创建。
|
||||
## tables_root 缺省 = res://data/tables;fail-fast 路径验证时可指向临时表目录。
|
||||
## 导出形态:顶层 counts 带两表行数计数 + enemy_rows/skill_rows 行字典原样(零新增字段、零改名;
|
||||
## 表内可省略字段缺席即缺席)。本件零随机、零数值硬编码、零判定逻辑。
|
||||
## fail-fast:表加载失败(缺表/行域校验/跨表悬空引用)、行缺关键字段、pool.skill_id 在技能表无对应行
|
||||
## → 逐条报错并以退出码 1 中止,禁软回退。
|
||||
extends SceneTree
|
||||
|
||||
const DEFAULT_OUT := "../sim-data/compendium/dump.json" # 相对 res://(game/ 上一级 = 仓库根)
|
||||
const DEFAULT_TABLES_ROOT := "res://data/tables"
|
||||
|
||||
# 行关键字段(缺一即 fail-fast):取两表 schema 加载期必在的键——
|
||||
# enemy 的 unit_count_max/atk_min/atk_max/attack_type/armor_type/armor_class 与技能行的
|
||||
# trigger/buff_ref/buff_duration/damage_mult/rider_mult 均为 schema 可省略字段,不列入。
|
||||
const ENEMY_REQUIRED_KEYS := ["enemy_id", "display_name", "row_kind", "level_index", "depth_band", "unit_count", "hp", "atk", "pool"]
|
||||
const SKILL_REQUIRED_KEYS := ["skill_id", "display_name", "type", "cooldown_beats"]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var opt := _parse_args(OS.get_cmdline_user_args())
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables(String(opt.tables_root), false):
|
||||
_print_diags(db)
|
||||
push_error("compendium_dump: 表加载失败(fail-fast)")
|
||||
quit(1)
|
||||
return
|
||||
var enemy_rows: Array = db.combat.enemy.rows
|
||||
var skill_rows: Array = db.combat.skill.rows
|
||||
var errs := _validate_rows(enemy_rows, skill_rows)
|
||||
if not errs.is_empty():
|
||||
for e in errs:
|
||||
push_error("compendium_dump: %s" % e)
|
||||
quit(1)
|
||||
return
|
||||
var payload := {
|
||||
"counts": {"enemy_rows": enemy_rows.size(), "skill_rows": skill_rows.size()},
|
||||
"enemy_rows": enemy_rows,
|
||||
"skill_rows": skill_rows,
|
||||
}
|
||||
var out_path := String(opt.out)
|
||||
var dir := out_path.get_base_dir()
|
||||
if not DirAccess.dir_exists_absolute(dir):
|
||||
var mk := DirAccess.make_dir_recursive_absolute(dir)
|
||||
if mk != OK:
|
||||
push_error("compendium_dump: 目录创建失败 %s(err=%d)" % [dir, mk])
|
||||
quit(1)
|
||||
return
|
||||
var f := FileAccess.open(out_path, FileAccess.WRITE)
|
||||
if f == null:
|
||||
push_error("compendium_dump: 导出文件打开失败 %s(err=%d)" % [out_path, FileAccess.get_open_error()])
|
||||
quit(1)
|
||||
return
|
||||
f.store_string(JSON.stringify(payload))
|
||||
f.close()
|
||||
print("图鉴导出完成: enemy_rows=%d skill_rows=%d → %s" % [enemy_rows.size(), skill_rows.size(), out_path])
|
||||
quit(0)
|
||||
|
||||
|
||||
func _parse_args(args: PackedStringArray) -> Dictionary:
|
||||
var opt := {"out": "", "tables_root": DEFAULT_TABLES_ROOT}
|
||||
for a in args:
|
||||
var kv := String(a).split("=", true, 1)
|
||||
if kv.size() != 2:
|
||||
continue
|
||||
if kv[0] == "out":
|
||||
opt["out"] = String(kv[1])
|
||||
elif kv[0] == "tables_root":
|
||||
opt["tables_root"] = String(kv[1])
|
||||
if String(opt.out).is_empty():
|
||||
opt["out"] = ProjectSettings.globalize_path("res://").path_join(DEFAULT_OUT)
|
||||
return opt
|
||||
|
||||
|
||||
# 行面 fail-fast 校验:关键字段在位 + pool.skill_id 引用闭合(技能表行集先建索引再对账)
|
||||
func _validate_rows(enemy_rows: Array, skill_rows: Array) -> Array:
|
||||
var errs: Array = []
|
||||
var skill_ids := {}
|
||||
for row_v in skill_rows:
|
||||
var row: Dictionary = row_v
|
||||
var sid := String(row.get("skill_id", ""))
|
||||
if sid.is_empty():
|
||||
errs.append("技能行缺 skill_id")
|
||||
for k in SKILL_REQUIRED_KEYS:
|
||||
if not row.has(k):
|
||||
errs.append("技能行 %s 缺关键字段: %s" % [sid, k])
|
||||
skill_ids[sid] = true
|
||||
for row_v in enemy_rows:
|
||||
var row: Dictionary = row_v
|
||||
var eid := String(row.get("enemy_id", ""))
|
||||
if eid.is_empty():
|
||||
errs.append("敌行缺 enemy_id")
|
||||
for k in ENEMY_REQUIRED_KEYS:
|
||||
if not row.has(k):
|
||||
errs.append("敌行 %s 缺关键字段: %s" % [eid, k])
|
||||
for entry_v in row.get("pool", []):
|
||||
var entry: Dictionary = entry_v
|
||||
var sid := String(entry.get("skill_id", ""))
|
||||
if not skill_ids.has(sid):
|
||||
errs.append("敌行 %s pool 引用不存在的 skill_id: %s(悬空致命)" % [eid, sid])
|
||||
return errs
|
||||
|
||||
|
||||
func _print_diags(db) -> void:
|
||||
for d in db.diagnostics():
|
||||
print("[ConfigDB/%s] %s: %s" % [d.category, d.severity, d.message])
|
||||
@@ -0,0 +1 @@
|
||||
uid://bnbug3wg477av
|
||||
Reference in new issue
Block a user