Files
ChildHood/tools/level-builder/report/report_generator.py
T
starmen1265 0f96b79dba feat(tools): 白模关卡自动生成Python脚本——Level Builder
两阶段流水线:搭建指南Markdown → JSON → UE5 Actor
- 解析器:4种表格变体+范围展开+坐标表达式计算
- 配置:37种BSP资产映射+13种材质+7区域定义
- 验证:265个Actor全部通过,0错误
- Builder:UE5 Python API自动放置Actor+材质+Outliner
- 报告:自动生成搭建报告Markdown

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 23:03:45 +08:00

160 lines
5.7 KiB
Python
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.
"""搭建报告生成器。
从解析数据和构建日志生成 Markdown 格式的搭建报告,
格式与 docs/technical/关卡-v01-搭建报告.md 一致。
"""
import json
from pathlib import Path
from datetime import datetime
from typing import Optional
def generate_report(
level_data: dict,
build_log: Optional[list[dict]] = None,
output_path: str = "",
) -> str:
"""生成搭建报告 Markdown。
Args:
level_data: 解析器输出的关卡数据(含 metadata/actors/summary)。
build_log: 构建日志(含每个 Actor 的成功/失败状态)。可选。
output_path: 报告输出路径。为空则只返回字符串。
Returns:
Markdown 格式的搭建报告文本。
"""
actors = level_data.get("actors", [])
summary = level_data.get("summary", {})
metadata = level_data.get("metadata", {})
lines = []
lines.append("# 白模搭建报告 — 自动生成")
lines.append("")
lines.append(f"> 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
lines.append(f"> 数据来源: {metadata.get('source', '未知')}")
lines.append(f"> 场景尺寸: {metadata.get('scene_size', ['?','?'])[0]}cm × {metadata.get('scene_size', ['?','?'])[1]}cm")
lines.append("")
# ---- 搭建概要 ----
lines.append("## 一、搭建概要")
lines.append("")
total = summary.get("total_actors", len(actors))
by_category = summary.get("by_category", {})
lines.append(f"- **总 Actor 数**: {total}")
for cat in ["STR", "PROP", "INTR", "COLL", "BND", "LGT", "PLAY"]:
count = by_category.get(cat, 0)
if count > 0:
cat_name = {"STR": "结构体", "PROP": "布景", "INTR": "交互物",
"COLL": "收集品", "BND": "边界", "LGT": "光源", "PLAY": "PlayerStart"}
lines.append(f"- {cat_name.get(cat, cat)} ({cat}): {count}")
if build_log:
ok = sum(1 for b in build_log if b.get("status") == "ok")
failed = sum(1 for b in build_log if b.get("status") != "ok")
lines.append(f"- 构建成功: {ok}")
lines.append(f"- 构建失败: {failed}")
lines.append("")
# ---- 逐区域对照 ----
lines.append("## 二、逐区域对照")
lines.append("")
by_zone = summary.get("by_zone", {})
zone_names = {
"Z1": "家", "Z2": "小卖部", "Z3": "主街道",
"Z4": "空地", "Z5": "巷子", "Z6": "水塘边", "Z7": "老居民楼",
"GLOBAL": "全局",
}
for zone_id in ["Z1", "Z2", "Z3", "Z4", "Z5", "Z6", "Z7", "GLOBAL"]:
zone_actors = [a for a in actors if a.get("zone_id") == zone_id]
if not zone_actors:
continue
zone_name = zone_names.get(zone_id, zone_id)
lines.append(f"### {zone_id} {zone_name}")
lines.append("")
lines.append(f"共 {len(zone_actors)} 个 Actor")
lines.append("")
lines.append("| # | Actor 名称 | 资产 | 位置 | 缩放 | 材质 | 状态 |")
lines.append("|---|-----------|------|------|------|------|------|")
for j, a in enumerate(zone_actors):
name = a.get("actor_name", "")
asset = a.get("asset_key", "—")
loc = a.get("location", [0, 0, 0])
loc_str = f"({loc[0]:.0f},{loc[1]:.0f},{loc[2]:.0f})"
scale = a.get("scale", [1, 1, 1])
if scale == [1, 1, 1] or scale == (1, 1, 1):
scale_str = "—"
else:
scale_str = f"({scale[0]:.2f},{scale[1]:.2f},{scale[2]:.2f})"
mat = a.get("material", "—") or "—"
# 状态
status = "✅"
if build_log:
log_entry = next((b for b in build_log if b.get("name") == name), None)
if log_entry:
status = "✅" if log_entry.get("status") == "ok" else f"❌ {log_entry.get('reason', '')}"
lines.append(f"| {j+1} | {name} | {asset} | {loc_str} | {scale_str} | {mat} | {status} |")
lines.append("")
# ---- 资产使用统计 ----
lines.append("## 三、资产使用统计")
lines.append("")
asset_usage = {}
for a in actors:
key = a.get("asset_key", "")
if key:
asset_usage[key] = asset_usage.get(key, 0) + 1
lines.append("| 资产名 | 使用次数 |")
lines.append("|--------|---------|")
for key in sorted(asset_usage.keys(), key=lambda k: -asset_usage[k]):
lines.append(f"| {key} | {asset_usage[key]} |")
lines.append("")
# ---- 缩放记录 ----
lines.append("## 四、缩放记录")
lines.append("")
lines.append("| Actor 名称 | 资产 | 缩放 | 实际尺寸 (cm) |")
lines.append("|-----------|------|------|-------------|")
for a in actors:
scale = a.get("scale", [1, 1, 1])
if scale != [1, 1, 1] and scale != (1, 1, 1):
name = a.get("actor_name", "")
asset_key = a.get("asset_key", "")
# 计算实际尺寸
from config.asset_mapping import MESH_ASSETS
default_size = MESH_ASSETS.get(asset_key, {}).get("default_size", (100, 100, 100))
if default_size:
actual = (
f"{default_size[0]*scale[0]:.0f}×"
f"{default_size[1]*scale[1]:.0f}×"
f"{default_size[2]*scale[2]:.0f}"
)
else:
actual = "(默认尺寸未知)"
lines.append(f"| {name} | {asset_key} | ({scale[0]:.2f},{scale[1]:.2f},{scale[2]:.2f}) | {actual} |")
lines.append("")
report = "\n".join(lines)
# 写入文件
if output_path:
Path(output_path).write_text(report, encoding="utf-8")
print(f"报告已写入: {output_path}")
return report