合并(工程批·图鉴外置工具·用户令): compendium_dump经ConfigDB导出enemy+skills全行JSON(out/tables_root参数化)+compendium_gen生成自包含HTML图鉴(按关按带分组·池份额·技能语义); 对账三件+悬空引用fail-fast非零退出+件核直跑复验(28敌卡·退出码核)过; 结构审计PASS
ci / gates (push) Successful in 8m1s
ci / gates (push) Successful in 8m1s
This commit is contained in:
commit
70424e0d06
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
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""怪物图鉴外置工具·渲染端——读 dump.json,生成自包含 index.html(零判定逻辑,只做组织与呈现)。
|
||||||
|
|
||||||
|
数据源 = game/sim/compendium_dump.gd 的导出件(enemy + skills 两表行数据原样 + 两表行数计数)。
|
||||||
|
组织:按 level_index 分关、关内按 depth_band 分组、组内保持表内行序;每行一张敌种卡——
|
||||||
|
显示名 / enemy_id / row_kind / 全部属性行值 / 池构成(技能显示名 + 权重 + 份额)+ 每技能语义
|
||||||
|
(类型 / 倍率 / 必中附加分量 / 冷却 / 触发条件 / 增益引用与持续)。
|
||||||
|
自包含:纯 python 标准库、内容全部内嵌标记、无外链无网络请求无构建工具链,双击可开;
|
||||||
|
同路径重复生成直接覆盖。字段名保留英文原文,值尽量用表内中文 display_name 与枚举中文名。
|
||||||
|
用法:
|
||||||
|
python tools/compendium_gen.py # 缺省 dump = sim-data/compendium/dump.json,输出同目录 index.html
|
||||||
|
python tools/compendium_gen.py --dump <路径> --out <路径>
|
||||||
|
fail-fast:dump 缺失 / JSON 损坏 / counts 与实际行数不符 / 行缺展示必需字段 / 池引用悬空
|
||||||
|
→ 报错退出码 1,禁软回退。
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from html import escape
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import NoReturn
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
DEFAULT_DUMP = ROOT / "sim-data" / "compendium" / "dump.json"
|
||||||
|
DEFAULT_OUT = ROOT / "sim-data" / "compendium" / "index.html"
|
||||||
|
|
||||||
|
# 展示必需字段(dump 采集端 fail-fast 契约的对应面;缺一即死)
|
||||||
|
ENEMY_REQUIRED = ("enemy_id", "display_name", "row_kind", "level_index", "depth_band")
|
||||||
|
SKILL_REQUIRED = ("skill_id", "display_name", "type", "cooldown_beats")
|
||||||
|
|
||||||
|
# 以下中文名均为表 schema 封闭枚举的展示词(enemy_table.gd / skill_table.gd),非玩法数值
|
||||||
|
LEVEL_CN = {1: "第一关", 2: "第二关", 3: "第三关"}
|
||||||
|
ROW_KIND_CN = {"battle": "战斗行", "boss": "Boss行"}
|
||||||
|
ATTACK_TYPE_CN = {"normal": "普通", "pierce": "穿刺", "magic": "魔法"}
|
||||||
|
ARMOR_TYPE_CN = {"light": "轻甲", "ward": "魔法护甲", "heavy": "重甲"}
|
||||||
|
SKILL_TYPE_CN = {"basic": "普攻", "damage": "伤害", "buff": "增益", "debuff": "妨害"}
|
||||||
|
TRIGGER_FACT_CN = {
|
||||||
|
"self_hp_pct": "自身血量%", "hero_hp_pct": "主角血量%", "round_index": "回合序号",
|
||||||
|
"last_round_end": "上轮结束", "mines_hit_last": "上轮踩雷数", "mines_hit_total": "累计踩雷数",
|
||||||
|
}
|
||||||
|
TRIGGER_OP_CN = {"eq": "==", "neq": "≠", "gt": ">", "gte": "≥", "lt": "<", "lte": "≤"}
|
||||||
|
|
||||||
|
FIELD_CN = {
|
||||||
|
"level_index": "关卡", "row_kind": "行类", "depth_band": "深度带", "enemy_id": "敌种ID",
|
||||||
|
"display_name": "显示名", "unit_count": "单位数", "unit_count_max": "单位数上限",
|
||||||
|
"hp": "生命", "atk": "攻击基线", "atk_min": "攻击下限", "atk_max": "攻击上限",
|
||||||
|
"attack_type": "攻击类型", "armor_type": "护甲类型", "armor_class": "护甲等级",
|
||||||
|
"skill_id": "技能ID", "type": "类型", "damage_mult": "伤害倍率", "rider_mult": "必中附加分量",
|
||||||
|
"cooldown_beats": "冷却拍数", "trigger": "触发条件", "buff_ref": "增益引用", "buff_duration": "增益持续",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def die(msg: str) -> NoReturn:
|
||||||
|
print(f"[compendium_gen] FAIL:{msg}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def esc(v) -> str:
|
||||||
|
return escape(str(v), quote=True)
|
||||||
|
|
||||||
|
|
||||||
|
def enum_cn(table: dict, key: str, cn_map: dict) -> str:
|
||||||
|
"""枚举值 → 「中文名 (raw)」;未知枚举原样展示(fail-fast 只拦必需字段,展示层不发明)"""
|
||||||
|
raw = table.get(key, "")
|
||||||
|
cn = cn_map.get(raw)
|
||||||
|
return f"{cn} ({raw})" if cn is not None else str(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def level_cn(level_index) -> str:
|
||||||
|
try:
|
||||||
|
n = int(level_index)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return esc(level_index)
|
||||||
|
return LEVEL_CN.get(n, f"第{n}关")
|
||||||
|
|
||||||
|
|
||||||
|
def trigger_desc(trig) -> str:
|
||||||
|
if not isinstance(trig, dict):
|
||||||
|
die(f"trigger 字段须为字典,实际: {trig!r}")
|
||||||
|
fact = trig.get("fact", "")
|
||||||
|
op = trig.get("op", "")
|
||||||
|
parts = [f"{TRIGGER_FACT_CN.get(fact, fact)} {TRIGGER_OP_CN.get(op, op)} {trig.get('value', '')}".strip()]
|
||||||
|
if trig.get("latch"):
|
||||||
|
parts.append("一次性(latch)")
|
||||||
|
return " · ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def skill_semantics(srow: dict) -> str:
|
||||||
|
bits = [enum_cn(srow, "type", SKILL_TYPE_CN)]
|
||||||
|
if "damage_mult" in srow:
|
||||||
|
bits.append(f"倍率 ×{srow['damage_mult']}")
|
||||||
|
if "rider_mult" in srow:
|
||||||
|
bits.append(f"必中附加 ×{srow['rider_mult']}")
|
||||||
|
bits.append(f"冷却 {srow.get('cooldown_beats', '?')} 拍")
|
||||||
|
if "trigger" in srow:
|
||||||
|
bits.append(f"触发: {trigger_desc(srow['trigger'])}")
|
||||||
|
if "buff_ref" in srow:
|
||||||
|
dur = f" · 持续 {srow['buff_duration']}" if "buff_duration" in srow else ""
|
||||||
|
bits.append(f"增益引用 {srow['buff_ref']}{dur}")
|
||||||
|
return " · ".join(bits)
|
||||||
|
|
||||||
|
|
||||||
|
def attr_value(row: dict, key: str) -> str:
|
||||||
|
if key == "row_kind":
|
||||||
|
return enum_cn(row, key, ROW_KIND_CN)
|
||||||
|
if key == "attack_type":
|
||||||
|
return enum_cn(row, key, ATTACK_TYPE_CN)
|
||||||
|
if key == "armor_type":
|
||||||
|
return enum_cn(row, key, ARMOR_TYPE_CN)
|
||||||
|
return esc(row[key])
|
||||||
|
|
||||||
|
|
||||||
|
def render_enemy_card(row: dict, skill_by_id: dict) -> str:
|
||||||
|
eid = row["enemy_id"]
|
||||||
|
badge = ROW_KIND_CN.get(row["row_kind"], row["row_kind"])
|
||||||
|
out = [f'<section class="enemy" data-enemy-row="{esc(eid)}">']
|
||||||
|
out.append(f'<h4>{esc(row["display_name"])} <code>{esc(eid)}</code>'
|
||||||
|
f'<span class="badge">{esc(badge)}</span></h4>')
|
||||||
|
out.append('<table class="attrs"><tbody>')
|
||||||
|
for key in row:
|
||||||
|
if key == "pool":
|
||||||
|
continue
|
||||||
|
out.append(f'<tr><th>{esc(FIELD_CN.get(key, key))} <code>{esc(key)}</code></th>'
|
||||||
|
f'<td>{attr_value(row, key)}</td></tr>')
|
||||||
|
out.append('</tbody></table>')
|
||||||
|
pool = row.get("pool")
|
||||||
|
if not isinstance(pool, list) or not pool:
|
||||||
|
die(f"敌行 {eid} pool 缺失或非数组(dump 采集端契约破坏)")
|
||||||
|
weights = []
|
||||||
|
for entry in pool:
|
||||||
|
if not isinstance(entry, dict) or "skill_id" not in entry or "weight" not in entry:
|
||||||
|
die(f"敌行 {eid} pool 条目缺 skill_id/weight: {entry!r}")
|
||||||
|
weights.append(int(entry["weight"]))
|
||||||
|
total = sum(weights)
|
||||||
|
if total <= 0:
|
||||||
|
die(f"敌行 {eid} pool 权重合计非正: {total}")
|
||||||
|
out.append('<table class="pool"><thead><tr>'
|
||||||
|
'<th>技能</th><th>权重</th><th>份额</th><th>语义</th></tr></thead><tbody>')
|
||||||
|
for entry, w in zip(pool, weights):
|
||||||
|
sid = str(entry["skill_id"])
|
||||||
|
if sid not in skill_by_id:
|
||||||
|
die(f"敌行 {eid} pool 引用不存在的 skill_id: {sid}(悬空致命)")
|
||||||
|
srow = skill_by_id[sid]
|
||||||
|
share = f"{w / total * 100:.1f}%"
|
||||||
|
out.append(f'<tr><td>{esc(srow["display_name"])} <code>{esc(sid)}</code></td>'
|
||||||
|
f'<td>{w}</td><td>{share}</td>'
|
||||||
|
f'<td class="sem">{skill_semantics(srow)}</td></tr>')
|
||||||
|
out.append('</tbody></table></section>')
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
CSS = """\
|
||||||
|
body{font-family:"Microsoft YaHei",system-ui,sans-serif;margin:2em auto;max-width:60em;
|
||||||
|
padding:0 1em;color:#222;background:#faf8f4;line-height:1.5}
|
||||||
|
h1{border-bottom:3px solid #6b5a3e;padding-bottom:.2em}
|
||||||
|
h2{margin-top:1.6em;color:#4a3b26}
|
||||||
|
h3{margin:1.2em 0 .4em;color:#5a4a30}
|
||||||
|
section.enemy{border:1px solid #c9bda5;border-radius:6px;padding:.6em 1em;margin:.8em 0;background:#fff}
|
||||||
|
section.enemy h4{margin:.2em 0 .5em}
|
||||||
|
code{background:#efe9dc;border-radius:3px;padding:0 .3em;font-size:.9em}
|
||||||
|
.badge{float:right;background:#6b5a3e;color:#fff;border-radius:3px;padding:.05em .5em;font-size:.8em}
|
||||||
|
table{border-collapse:collapse;width:100%;margin:.3em 0}
|
||||||
|
th,td{border:1px solid #d8cdb8;padding:.25em .6em;text-align:left;vertical-align:top}
|
||||||
|
th{background:#f1ebdd;white-space:nowrap;font-weight:600}
|
||||||
|
table.pool th{background:#e7dfd0}
|
||||||
|
td.sem{font-size:.92em}
|
||||||
|
p.note{color:#6a5c44;font-size:.92em}
|
||||||
|
.counts{background:#efe9dc;border-radius:4px;padding:.3em .8em;display:inline-block}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def build_html(dump: dict) -> str:
|
||||||
|
counts = dump.get("counts")
|
||||||
|
enemy_rows = dump.get("enemy_rows")
|
||||||
|
skill_rows = dump.get("skill_rows")
|
||||||
|
if not isinstance(counts, dict) or not isinstance(enemy_rows, list) or not isinstance(skill_rows, list):
|
||||||
|
die("dump.json 顶层缺 counts/enemy_rows/skill_rows(结构不符)")
|
||||||
|
if counts.get("enemy_rows") != len(enemy_rows) or counts.get("skill_rows") != len(skill_rows):
|
||||||
|
die(f"counts 与行数不符: counts={counts} 实际 enemy={len(enemy_rows)} skill={len(skill_rows)}")
|
||||||
|
|
||||||
|
skill_by_id = {}
|
||||||
|
for r in skill_rows:
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
die(f"技能行非字典: {r!r}")
|
||||||
|
for k in SKILL_REQUIRED:
|
||||||
|
if k not in r:
|
||||||
|
die(f"技能行缺必需字段 {k}: {r!r}")
|
||||||
|
skill_by_id[str(r["skill_id"])] = r
|
||||||
|
|
||||||
|
for r in enemy_rows:
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
die(f"敌行非字典: {r!r}")
|
||||||
|
for k in ENEMY_REQUIRED:
|
||||||
|
if k not in r:
|
||||||
|
die(f"敌行缺必需字段 {k}: {r!r}")
|
||||||
|
|
||||||
|
levels = {}
|
||||||
|
for r in enemy_rows:
|
||||||
|
levels.setdefault(int(r["level_index"]), []).append(r)
|
||||||
|
|
||||||
|
out = ['<!DOCTYPE html>', '<html lang="zh-CN">', '<head>',
|
||||||
|
'<meta charset="utf-8">', '<title>怪物图鉴</title>',
|
||||||
|
f'<style>{CSS}</style>', '</head>', '<body>',
|
||||||
|
'<h1>怪物图鉴</h1>',
|
||||||
|
'<p class="note">本页由外置图鉴工具生成(tools/compendium_gen.py),数据来自 '
|
||||||
|
'sim-data/compendium/dump.json(game/data/tables/enemy.tres 与 skills.tres 经 ConfigDB '
|
||||||
|
'单点读取导出)。只读对账面,非游戏内界面;重复生成直接覆盖。</p>',
|
||||||
|
f'<p class="counts">敌行 <strong>{len(enemy_rows)}</strong> 行 · 技行 '
|
||||||
|
f'<strong>{len(skill_rows)}</strong> 行(counts 计数核对一致)</p>']
|
||||||
|
|
||||||
|
for lv in sorted(levels):
|
||||||
|
rows = levels[lv]
|
||||||
|
out.append(f'<h2>{level_cn(lv)} <code>level_index={lv}</code>({len(rows)} 行)</h2>')
|
||||||
|
bands = {}
|
||||||
|
for r in rows:
|
||||||
|
bands.setdefault(int(r["depth_band"]), []).append(r)
|
||||||
|
for band in sorted(bands):
|
||||||
|
out.append(f'<h3>深度带 {band} <code>depth_band={band}</code></h3>')
|
||||||
|
for r in bands[band]:
|
||||||
|
out.append(render_enemy_card(r, skill_by_id))
|
||||||
|
out.extend(['</body>', '</html>'])
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(description="怪物图鉴渲染端:dump.json → 自包含 index.html")
|
||||||
|
ap.add_argument("--dump", default=str(DEFAULT_DUMP), help="采集端导出 JSON 路径")
|
||||||
|
ap.add_argument("--out", default=str(DEFAULT_OUT), help="输出 HTML 路径")
|
||||||
|
args = ap.parse_args()
|
||||||
|
dump_path, out_path = Path(args.dump), Path(args.out)
|
||||||
|
if not dump_path.is_file():
|
||||||
|
die(f"dump.json 不存在: {dump_path}")
|
||||||
|
try:
|
||||||
|
dump = json.loads(dump_path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError) as e:
|
||||||
|
die(f"dump.json 读取/解析失败: {e}")
|
||||||
|
html = build_html(dump)
|
||||||
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_path.write_text(html, encoding="utf-8")
|
||||||
|
n_enemy = len(dump["enemy_rows"])
|
||||||
|
n_skill = len(dump["skill_rows"])
|
||||||
|
print(f"[compendium_gen] 完成: 敌行 {n_enemy} · 技行 {n_skill} → {out_path}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in new issue
Block a user