"""搭建报告生成器。 从解析数据和构建日志生成 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