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>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
"""搭建报告生成器。
|
||||
|
||||
从解析数据和构建日志生成 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
|
||||
Reference in New Issue
Block a user