feat(tools): 新增execution-to-guide执行案转搭建指南工具 #35
@@ -2,6 +2,8 @@
|
||||
|
||||
> 本模板供策划产出关卡执行案时使用。关卡执行案是场景设计案的"空间精度版本"——将氛围描述转化为带有坐标、尺寸和资产建议的结构化搭建指南,供程序在 UE5 编辑器中搭建白模。
|
||||
>
|
||||
> **注意**:交互物和收集品的放置**不在本模板范围内**,由交互系统/收集系统后续在白盒关卡上叠加。
|
||||
>
|
||||
> 文件命名:`关卡-v<XX>-<名称>-执行案.md`(如 `关卡-v01-城郊街区-执行案.md`)
|
||||
> 存放位置:`docs/design/<阶段>/`(与其他执行案同目录)
|
||||
|
||||
@@ -131,30 +133,18 @@
|
||||
| Zx-P02 | | | | | |
|
||||
| ... | | | | | |
|
||||
|
||||
#### 3.x.4 可交互物体标记
|
||||
#### 3.x.4 组合资产标记
|
||||
|
||||
<!-- 引用交互系统执行案中的交互物,此处只标注空间位置 -->
|
||||
<!-- 需要多个 Primitives 拼搭的物件(无直接 BSP 匹配)。如无此类物件可删除本节。 -->
|
||||
|
||||
| 交互编号 | 物件名称 | 交互类型 | 相对原点偏移 (ΔX,ΔY,ΔZ) | 触发半径 (cm) | 引用 |
|
||||
|---------|---------|---------|----------------------|-------------|------|
|
||||
| Zx-I01 | <!-- 如 旧冰箱 --> | <!-- 如 观察 --> | (200,400,10) | <!-- 如 150 --> | 交互系统-执行案 #n |
|
||||
| Zx-I02 | | | | | |
|
||||
| ... | | | | | |
|
||||
|
||||
> 交互类型参照交互系统执行案中的分类(观察/拾取/操作/对话等)。
|
||||
> "引用"列填写交互系统执行案中对应交互物的编号,确保跨文档一致。
|
||||
|
||||
#### 3.x.5 收集品标记
|
||||
|
||||
<!-- 引用收集系统执行案中的收集品,此处只标注空间位置 -->
|
||||
|
||||
| 收集编号 | 物品名称 | 稀有度 | 相对原点偏移 (ΔX,ΔY,ΔZ) | 引用 |
|
||||
|---------|---------|--------|----------------------|------|
|
||||
| Zx-C01 | <!-- 如 彩色弹珠 --> | <!-- 如 普通/稀有 --> | (500,200,0) | 收集系统-执行案 #n |
|
||||
| 编号 | 物件名称 | 期望尺寸 (约, cm) | 推荐拼搭方案 | 说明 |
|
||||
|------|---------|-----------------|-------------|------|
|
||||
| Zx-C01 | <!-- 如 旧收音机 --> | <!-- 如 20x15x12 --> | <!-- 如 Cube(0.2,0.15,0.12) + Cylinder天线 --> | <!-- 如 放在柜台上 --> |
|
||||
| Zx-C02 | | | | |
|
||||
| ... | | | | | |
|
||||
|
||||
> 稀有度影响拾取物的视觉效果标识(普通=白色光,稀有=金色光),具体表现由收集系统技术方案定义。
|
||||
> 组合资产将在搭建指南生成阶段展开为多个 Primitive Actor,并在组合资产清单中记录拼搭方案。
|
||||
> 用户确认后的组合资产方案将回馈到 asset-catalog 模板库,供后续关卡直接引用。
|
||||
|
||||
---
|
||||
|
||||
@@ -183,14 +173,6 @@
|
||||
| G-E02 | | | | | |
|
||||
| ... | | | | | |
|
||||
|
||||
### 4.3 收集品位置汇总
|
||||
|
||||
| # | 物品名称 | 所在区域 | 绝对坐标 (X,Y,Z) | 稀有度 | 引用 |
|
||||
|---|---------|---------|-----------------|--------|------|
|
||||
| 1 | | | | | 收集系统-执行案 #n |
|
||||
| 2 | | | | | |
|
||||
| ... | | | | | |
|
||||
|
||||
---
|
||||
|
||||
## 五、场景边界与遮挡
|
||||
@@ -225,10 +207,10 @@
|
||||
### 策划填写要点
|
||||
|
||||
1. **先确定区域坐标布局**(第二章),确保所有区域在总尺寸内不重叠、流线合理
|
||||
2. **逐区域填写**(第三章),按"结构体 → 布景 → 交互物 → 收集品"的顺序
|
||||
2. **逐区域填写**(第三章),按"结构体 → 布景 → 组合资产"的顺序
|
||||
3. **BSP 资产建议是方向性指引**:写资产类别名和组合方式即可(如 "Wall_400x300 × 2"),不需要写文件路径
|
||||
4. **尺寸以 cm 为单位**,对应 UE5 的 1 uu = 1 cm
|
||||
5. **交叉引用**:交互物和收集品必须引用对应系统执行案中的编号,保持跨文档一致
|
||||
5. **组合资产**:无法用单个 BSP 资产匹配的物件,在 3.x.4 中记录拼搭方案
|
||||
|
||||
### 不要填写的内容
|
||||
|
||||
@@ -237,6 +219,7 @@
|
||||
- ❌ Blueprint 类名
|
||||
- ❌ 精确的硬编码参数值(标注"建议值,可调"并附范围)
|
||||
- ❌ 代码或脚本
|
||||
- ❌ 交互物和收集品位置(由系统后续叠加)
|
||||
|
||||
### 与场景设计案的关系
|
||||
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
# v0.1 白模搭建操作指南 — 城郊小街
|
||||
|
||||
> 本文档由 execution-to-guide 工具自动生成,供 level-builder 搭建白模关卡使用。
|
||||
> 基于: 关卡-v0.1-城郊小街-执行案.md
|
||||
|
||||
---
|
||||
|
||||
## 〇、全局信息
|
||||
|
||||
### 场景参数
|
||||
|
||||
| 参数 | 值 |
|
||||
|------|-----|
|
||||
| 场景总尺寸 | 8000cm × 6000cm(80m × 60m) |
|
||||
| 地面高度基准 | Z = 0 |
|
||||
| 主角身高 | ~120cm |
|
||||
| 摄像机高度 | ~100cm |
|
||||
| 时间设定 | 夏日午后 15:00 |
|
||||
| 坐标原点 | 场景西南角地平面 |
|
||||
|
||||
### UE5 路径前缀
|
||||
|
||||
- **Mesh**: `Content/BlockingStarterPack/Meshes/`
|
||||
- **Material**: `Content/BlockingStarterPack/Materials/`
|
||||
- **Material (Basics)**: `Content/BlockingStarterPack/Materials/Blocking/Basics/`
|
||||
|
||||
### 命名规范
|
||||
|
||||
```
|
||||
LVL_<ZoneID>_<Category>_<Description>
|
||||
Category: STR | PROP | COMP | BND | LGT
|
||||
```
|
||||
|
||||
### Outliner 文件夹结构
|
||||
|
||||
```
|
||||
LVL_城郊小街/
|
||||
Z1_家/
|
||||
Structure/ Props/
|
||||
Z2_小卖部/
|
||||
Structure/ Props/
|
||||
Z3_主街道/
|
||||
Structure/ Props/
|
||||
Z4_空地/
|
||||
Structure/ Props/
|
||||
Z5_巷子/
|
||||
Structure/ Props/
|
||||
Z6_水塘边/
|
||||
Structure/ Props/
|
||||
Z7_老居民楼/
|
||||
Structure/ Props/
|
||||
Global/
|
||||
Boundaries/ Lighting/ PlayerStart/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、BSP 资产映射表
|
||||
|
||||
### 建筑 — 墙体
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| Wall_400x300 | `Architecture/Walls/SM_Wall_400x300` | 400×20×300 |
|
||||
| Wall_200x300 | `Architecture/Walls/SM_Wall_200x300` | 200×20×300 |
|
||||
| Wall_400x100 | `Architecture/Walls/SM_Wall_400x100` | 400×20×100 |
|
||||
| Wall_100x300 | `Architecture/Walls/SM_Wall_100x300` | 100×20×300 |
|
||||
|
||||
### 建筑 — 门墙
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| WallDoor_200x300 | `Architecture/Walls/SM_WallDoor_200x300` | 200×20×300 |
|
||||
|
||||
### 建筑 — 窗墙
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| WallWindow_400x300 | `Architecture/Walls/SM_WallWindow_400x300` | 400×20×300 |
|
||||
| WallWindowSmall_200x300 | `Architecture/Walls/SM_WallWindowSmall_200x300` | 200×20×300 |
|
||||
|
||||
### 建筑 — 地板
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| Floor_300x400 | `Architecture/Floors/SM_Floor_300x400` | 300×400×22 |
|
||||
| Floor_1000x1000 | `Architecture/Floors/SM_Floor_1000x1000` | 1000×1000×22 |
|
||||
| Floor_200x300 | `Architecture/Floors/SM_Floor_200x300` | 200×300×22 |
|
||||
|
||||
### 建筑 — 窗户
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| FakeWindowsFrame | `Architecture/WindowsSet/SM_FakeWindowsFrame` | 140×14×164 |
|
||||
|
||||
### 建筑 — 围挡
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| BarrierWall_600 | `Architecture/Barriers/SM_BarrierWall_600` | 600×60×210 |
|
||||
| BarrierWall_200 | `Architecture/Barriers/SM_BarrierWall_200` | 200×60×210 |
|
||||
|
||||
### 建筑 — 围栏
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| Fence_Small_400 | `Industrials/SM_Fence_Small_400` | 404×6×112 |
|
||||
|
||||
### 建筑 — 横梁
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| Beam_300 | `Architecture/Beams/SM_Beam_300` | 40×40×300 |
|
||||
|
||||
### 建筑 — 大型建筑
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| Building_01 | `BigBuildings/SM_Building_01` | 1950×1450×4010 |
|
||||
|
||||
### 家具
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| Monitor_01 | `Furnitures/SM_Monitor_01` | 54×18×37 |
|
||||
| SimpleTable_01 | `Furnitures/SM_SimpleTable_01` | 161×87×74 |
|
||||
| BedSingle | `Bedroom/SM_BedSingle` | 116×216×68 |
|
||||
| SimpleTable_02 | `Furnitures/SM_SimpleTable_02` | 239×99×74 |
|
||||
|
||||
### 工业/装饰
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| WoodenBox_Small | `Industrials/SM_WoodenBox_Small` | 70×70×70 |
|
||||
| Bucket_01 | `Decorations/SM_Bucket_01` | 30×27×26 |
|
||||
| Barrel | `Industrials/SM_Barrel` | 73×73×111 |
|
||||
| TrashCans_01 | `Trashs/SM_TrashCans_01` | 53×71×118 |
|
||||
| WoodenPallet_01 | `Industrials/SM_WoodenPallet_01` | 188×188×20 |
|
||||
| WoodenBox_Normal | `Industrials/SM_WoodenBox_Normal` | 90×90×90 |
|
||||
|
||||
### 载具
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| Car01 | `Vehicles/SM_Car01` | 160×420×180 |
|
||||
|
||||
### 植被
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| Tree_01 | `Vegetations/SM_Tree_01` | 762×803×845 |
|
||||
|
||||
### 灯具
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| RoadLamp_01 | `Lights/SM_RoadLamp_01` | 40×244×850 |
|
||||
|
||||
### 装饰
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| DisplayPanels_400x200 | `DisplayPanels/SM_DisplayPanels_400x200` | 400×8×200 |
|
||||
|
||||
### 其他
|
||||
|
||||
| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |
|
||||
|-------------|-------------|--------------|
|
||||
| WallDoorGarage_400x400 | `` | 0×0×0 |
|
||||
| _composite | `` | 0×0×0 |
|
||||
| Wheel | `` | 0×0×0 |
|
||||
| MI_DevGlassWhite | `` | 0×0×0 |
|
||||
| MI_DevGray | `` | 0×0×0 |
|
||||
| MI_DevWhite | `` | 0×0×0 |
|
||||
| MI_DevBlue | `` | 0×0×0 |
|
||||
| MI_DevYellow | `` | 0×0×0 |
|
||||
| MI_DevGlassBlue | `` | 0×0×0 |
|
||||
| MI_DevFoliage | `` | 0×0×0 |
|
||||
| MI_DevGreen | `` | 0×0×0 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 二、Phase B — 地面与边界
|
||||
|
||||
### B1. 铺设地面
|
||||
|
||||
#### 全局地面(MI_DevGray)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 |
|
||||
|---|-----------|------|-------------|------|------|
|
||||
| 1 | LVL_Global_STR_GroundFloor_01 | Floor_1000x1000 | (500,500,0) | — | MI_DevGray |
|
||||
| 2 | LVL_Global_STR_GroundFloor_02 | Floor_1000x1000 | (500,1500,0) | — | MI_DevGray |
|
||||
| 3 | LVL_Global_STR_GroundFloor_03 | Floor_1000x1000 | (500,2500,0) | — | MI_DevGray |
|
||||
| 4 | LVL_Global_STR_GroundFloor_04 | Floor_1000x1000 | (500,3500,0) | — | MI_DevGray |
|
||||
| 5 | LVL_Global_STR_GroundFloor_05 | Floor_1000x1000 | (500,4500,0) | — | MI_DevGray |
|
||||
| 6 | LVL_Global_STR_GroundFloor_06 | Floor_1000x1000 | (500,5500,0) | — | MI_DevGray |
|
||||
| 7 | LVL_Global_STR_GroundFloor_07 | Floor_1000x1000 | (1500,500,0) | — | MI_DevGray |
|
||||
| 8 | LVL_Global_STR_GroundFloor_08 | Floor_1000x1000 | (1500,1500,0) | — | MI_DevGray |
|
||||
| 9 | LVL_Global_STR_GroundFloor_09 | Floor_1000x1000 | (1500,2500,0) | — | MI_DevGray |
|
||||
| 10 | LVL_Global_STR_GroundFloor_10 | Floor_1000x1000 | (1500,3500,0) | — | MI_DevGray |
|
||||
| 11 | LVL_Global_STR_GroundFloor_11 | Floor_1000x1000 | (1500,4500,0) | — | MI_DevGray |
|
||||
| 12 | LVL_Global_STR_GroundFloor_12 | Floor_1000x1000 | (1500,5500,0) | — | MI_DevGray |
|
||||
| 13 | LVL_Global_STR_GroundFloor_13 | Floor_1000x1000 | (2500,500,0) | — | MI_DevGray |
|
||||
| 14 | LVL_Global_STR_GroundFloor_14 | Floor_1000x1000 | (2500,1500,0) | — | MI_DevGray |
|
||||
| 15 | LVL_Global_STR_GroundFloor_15 | Floor_1000x1000 | (2500,2500,0) | — | MI_DevGray |
|
||||
| 16 | LVL_Global_STR_GroundFloor_16 | Floor_1000x1000 | (2500,3500,0) | — | MI_DevGray |
|
||||
| 17 | LVL_Global_STR_GroundFloor_17 | Floor_1000x1000 | (2500,4500,0) | — | MI_DevGray |
|
||||
| 18 | LVL_Global_STR_GroundFloor_18 | Floor_1000x1000 | (2500,5500,0) | — | MI_DevGray |
|
||||
| 19 | LVL_Global_STR_GroundFloor_19 | Floor_1000x1000 | (3500,500,0) | — | MI_DevGray |
|
||||
| 20 | LVL_Global_STR_GroundFloor_20 | Floor_1000x1000 | (3500,1500,0) | — | MI_DevGray |
|
||||
| 21 | LVL_Global_STR_GroundFloor_21 | Floor_1000x1000 | (3500,2500,0) | — | MI_DevGray |
|
||||
| 22 | LVL_Global_STR_GroundFloor_22 | Floor_1000x1000 | (3500,3500,0) | — | MI_DevGray |
|
||||
| 23 | LVL_Global_STR_GroundFloor_23 | Floor_1000x1000 | (3500,4500,0) | — | MI_DevGray |
|
||||
| 24 | LVL_Global_STR_GroundFloor_24 | Floor_1000x1000 | (3500,5500,0) | — | MI_DevGray |
|
||||
| 25 | LVL_Global_STR_GroundFloor_25 | Floor_1000x1000 | (4500,500,0) | — | MI_DevGray |
|
||||
| 26 | LVL_Global_STR_GroundFloor_26 | Floor_1000x1000 | (4500,1500,0) | — | MI_DevGray |
|
||||
| 27 | LVL_Global_STR_GroundFloor_27 | Floor_1000x1000 | (4500,2500,0) | — | MI_DevGray |
|
||||
| 28 | LVL_Global_STR_GroundFloor_28 | Floor_1000x1000 | (4500,3500,0) | — | MI_DevGray |
|
||||
| 29 | LVL_Global_STR_GroundFloor_29 | Floor_1000x1000 | (4500,4500,0) | — | MI_DevGray |
|
||||
| 30 | LVL_Global_STR_GroundFloor_30 | Floor_1000x1000 | (4500,5500,0) | — | MI_DevGray |
|
||||
| 31 | LVL_Global_STR_GroundFloor_31 | Floor_1000x1000 | (5500,500,0) | — | MI_DevGray |
|
||||
| 32 | LVL_Global_STR_GroundFloor_32 | Floor_1000x1000 | (5500,1500,0) | — | MI_DevGray |
|
||||
| 33 | LVL_Global_STR_GroundFloor_33 | Floor_1000x1000 | (5500,2500,0) | — | MI_DevGray |
|
||||
| 34 | LVL_Global_STR_GroundFloor_34 | Floor_1000x1000 | (5500,3500,0) | — | MI_DevGray |
|
||||
| 35 | LVL_Global_STR_GroundFloor_35 | Floor_1000x1000 | (5500,4500,0) | — | MI_DevGray |
|
||||
| 36 | LVL_Global_STR_GroundFloor_36 | Floor_1000x1000 | (5500,5500,0) | — | MI_DevGray |
|
||||
| 37 | LVL_Global_STR_GroundFloor_37 | Floor_1000x1000 | (6500,500,0) | — | MI_DevGray |
|
||||
| 38 | LVL_Global_STR_GroundFloor_38 | Floor_1000x1000 | (6500,1500,0) | — | MI_DevGray |
|
||||
| 39 | LVL_Global_STR_GroundFloor_39 | Floor_1000x1000 | (6500,2500,0) | — | MI_DevGray |
|
||||
| 40 | LVL_Global_STR_GroundFloor_40 | Floor_1000x1000 | (6500,3500,0) | — | MI_DevGray |
|
||||
| 41 | LVL_Global_STR_GroundFloor_41 | Floor_1000x1000 | (6500,4500,0) | — | MI_DevGray |
|
||||
| 42 | LVL_Global_STR_GroundFloor_42 | Floor_1000x1000 | (6500,5500,0) | — | MI_DevGray |
|
||||
| 43 | LVL_Global_STR_GroundFloor_43 | Floor_1000x1000 | (7500,500,0) | — | MI_DevGray |
|
||||
| 44 | LVL_Global_STR_GroundFloor_44 | Floor_1000x1000 | (7500,1500,0) | — | MI_DevGray |
|
||||
| 45 | LVL_Global_STR_GroundFloor_45 | Floor_1000x1000 | (7500,2500,0) | — | MI_DevGray |
|
||||
| 46 | LVL_Global_STR_GroundFloor_46 | Floor_1000x1000 | (7500,3500,0) | — | MI_DevGray |
|
||||
| 47 | LVL_Global_STR_GroundFloor_47 | Floor_1000x1000 | (7500,4500,0) | — | MI_DevGray |
|
||||
| 48 | LVL_Global_STR_GroundFloor_48 | Floor_1000x1000 | (7500,5500,0) | — | MI_DevGray |
|
||||
|
||||
### B2. 放置边界围挡
|
||||
|
||||
#### 西边界
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||
|---|-----------|------|-------------|-------------|------|------|
|
||||
| 1 | LVL_Global_BND_西边界_01 | BarrierWall_600 | (0,300,105) | (0,90,0) | — | MI_DevRed |
|
||||
| 2 | LVL_Global_BND_西边界_02 | BarrierWall_600 | (0,900,105) | (0,90,0) | — | MI_DevRed |
|
||||
| 3 | LVL_Global_BND_西边界_03 | BarrierWall_600 | (0,1500,105) | (0,90,0) | — | MI_DevRed |
|
||||
| 4 | LVL_Global_BND_西边界_04 | BarrierWall_600 | (0,2100,105) | (0,90,0) | — | MI_DevRed |
|
||||
| 5 | LVL_Global_BND_西边界_05 | BarrierWall_600 | (0,2700,105) | (0,90,0) | — | MI_DevRed |
|
||||
| 6 | LVL_Global_BND_西边界_06 | BarrierWall_600 | (0,3300,105) | (0,90,0) | — | MI_DevRed |
|
||||
| 7 | LVL_Global_BND_西边界_07 | BarrierWall_600 | (0,3900,105) | (0,90,0) | — | MI_DevRed |
|
||||
| 8 | LVL_Global_BND_西边界_08 | BarrierWall_600 | (0,4500,105) | (0,90,0) | — | MI_DevRed |
|
||||
| 9 | LVL_Global_BND_西边界_09 | BarrierWall_600 | (0,5100,105) | (0,90,0) | — | MI_DevRed |
|
||||
| 10 | LVL_Global_BND_西边界_10 | BarrierWall_600 | (0,5700,105) | (0,90,0) | — | MI_DevRed |
|
||||
|
||||
#### 南边界
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||
|---|-----------|------|-------------|-------------|------|------|
|
||||
| 1 | LVL_Global_BND_南边界_01 | BarrierWall_600 | (300,0,105) | — | — | 各段标识色 |
|
||||
| 2 | LVL_Global_BND_南边界_02 | BarrierWall_600 | (900,0,105) | — | — | 各段标识色 |
|
||||
| 3 | LVL_Global_BND_南边界_03 | BarrierWall_600 | (1500,0,105) | — | — | 各段标识色 |
|
||||
| 4 | LVL_Global_BND_南边界_04 | BarrierWall_600 | (2100,0,105) | — | — | 各段标识色 |
|
||||
| 5 | LVL_Global_BND_南边界_05 | BarrierWall_600 | (2700,0,105) | — | — | 各段标识色 |
|
||||
| 6 | LVL_Global_BND_南边界_06 | BarrierWall_600 | (3300,0,105) | — | — | 各段标识色 |
|
||||
| 7 | LVL_Global_BND_南边界_07 | BarrierWall_600 | (3900,0,105) | — | — | 各段标识色 |
|
||||
| 8 | LVL_Global_BND_南边界_08 | BarrierWall_600 | (4500,0,105) | — | — | 各段标识色 |
|
||||
| 9 | LVL_Global_BND_南边界_09 | BarrierWall_600 | (5100,0,105) | — | — | 各段标识色 |
|
||||
| 10 | LVL_Global_BND_南边界_10 | BarrierWall_600 | (5700,0,105) | — | — | 各段标识色 |
|
||||
| 11 | LVL_Global_BND_南边界_11 | BarrierWall_600 | (6300,0,105) | — | — | 各段标识色 |
|
||||
| 12 | LVL_Global_BND_南边界_12 | BarrierWall_600 | (6900,0,105) | — | — | 各段标识色 |
|
||||
| 13 | LVL_Global_BND_南边界_13 | BarrierWall_600 | (7500,0,105) | — | — | 各段标识色 |
|
||||
|
||||
#### 东边界
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||
|---|-----------|------|-------------|-------------|------|------|
|
||||
| 1 | LVL_Global_BND_东边界_01 | BarrierWall_600 | (8000,300,105) | (0,90,0) | — | MI_DevBlue |
|
||||
| 2 | LVL_Global_BND_东边界_02 | BarrierWall_600 | (8000,900,105) | (0,90,0) | — | MI_DevBlue |
|
||||
| 3 | LVL_Global_BND_东边界_03 | BarrierWall_600 | (8000,1500,105) | (0,90,0) | — | MI_DevBlue |
|
||||
| 4 | LVL_Global_BND_东边界_04 | BarrierWall_600 | (8000,2100,105) | (0,90,0) | — | MI_DevBlue |
|
||||
| 5 | LVL_Global_BND_东边界_05 | BarrierWall_600 | (8000,2700,105) | (0,90,0) | — | MI_DevBlue |
|
||||
| 6 | LVL_Global_BND_东边界_06 | BarrierWall_600 | (8000,3300,105) | (0,90,0) | — | MI_DevBlue |
|
||||
| 7 | LVL_Global_BND_东边界_07 | BarrierWall_600 | (8000,3900,105) | (0,90,0) | — | MI_DevBlue |
|
||||
| 8 | LVL_Global_BND_东边界_08 | BarrierWall_600 | (8000,4500,105) | (0,90,0) | — | MI_DevBlue |
|
||||
| 9 | LVL_Global_BND_东边界_09 | BarrierWall_600 | (8000,5100,105) | (0,90,0) | — | MI_DevBlue |
|
||||
| 10 | LVL_Global_BND_东边界_10 | BarrierWall_600 | (8000,5700,105) | (0,90,0) | — | MI_DevBlue |
|
||||
|
||||
#### 北边界
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||
|---|-----------|------|-------------|-------------|------|------|
|
||||
| 1 | LVL_Global_BND_北边界_01 | BarrierWall_600 | (300,6000,105) | — | — | MI_DevBlue |
|
||||
| 2 | LVL_Global_BND_北边界_02 | BarrierWall_600 | (900,6000,105) | — | — | MI_DevBlue |
|
||||
| 3 | LVL_Global_BND_北边界_03 | BarrierWall_600 | (1500,6000,105) | — | — | MI_DevBlue |
|
||||
| 4 | LVL_Global_BND_北边界_04 | BarrierWall_600 | (2100,6000,105) | — | — | MI_DevBlue |
|
||||
| 5 | LVL_Global_BND_北边界_05 | BarrierWall_600 | (2700,6000,105) | — | — | MI_DevBlue |
|
||||
| 6 | LVL_Global_BND_北边界_06 | BarrierWall_600 | (3300,6000,105) | — | — | MI_DevBlue |
|
||||
| 7 | LVL_Global_BND_北边界_07 | BarrierWall_600 | (3900,6000,105) | — | — | MI_DevBlue |
|
||||
| 8 | LVL_Global_BND_北边界_08 | BarrierWall_600 | (4500,6000,105) | — | — | MI_DevBlue |
|
||||
| 9 | LVL_Global_BND_北边界_09 | BarrierWall_600 | (5100,6000,105) | — | — | MI_DevBlue |
|
||||
| 10 | LVL_Global_BND_北边界_10 | BarrierWall_600 | (5700,6000,105) | — | — | MI_DevBlue |
|
||||
| 11 | LVL_Global_BND_北边界_11 | BarrierWall_600 | (6300,6000,105) | — | — | MI_DevBlue |
|
||||
| 12 | LVL_Global_BND_北边界_12 | BarrierWall_600 | (6900,6000,105) | — | — | MI_DevBlue |
|
||||
| 13 | LVL_Global_BND_北边界_13 | BarrierWall_600 | (7500,6000,105) | — | — | MI_DevBlue |
|
||||
|
||||
#### Z6 水域
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||
|---|-----------|------|-------------|-------------|------|------|
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 三、Phase C — 逐区域搭建
|
||||
|
||||
### C1. Z1 — 家
|
||||
|
||||
#### C1.1 结构体(Structure)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||
|---|-----------|------|-------------|-------------|------|------|
|
||||
| | LVL_Z1_STR_South院Wall_01 | Wall_400x300 | (0,0,0) | — | — | MI_DevRed |
|
||||
| | LVL_Z1_STR_South院Wall_02 | Wall_400x300 | (400,0,0) | — | — | MI_DevRed |
|
||||
| | LVL_Z1_STR_South院Wall_03 | Wall_200x300 | (800,0,0) | — | — | MI_DevRed |
|
||||
| | LVL_Z1_STR_West院Wall_01 | Wall_400x300 | (0,0,0) | (0,-90,0) | — | MI_DevRed |
|
||||
| | LVL_Z1_STR_West院Wall_02 | Wall_400x300 | (400,0,0) | (0,-90,0) | — | MI_DevRed |
|
||||
| | LVL_Z1_STR_East院WallTop半段_01 | Wall_400x300 | (980,0,0) | (0,90,0) | — | MI_DevRed |
|
||||
| | LVL_Z1_STR_East院WallTop半段_02 | Wall_400x300 | (1380,0,0) | (0,90,0) | — | MI_DevRed |
|
||||
| | LVL_Z1_STR_North院Wall含院Door_01 | Cube | (0,780,0) | (0,180,0) | — | MI_DevRed |
|
||||
| | LVL_Z1_STR_房屋SouthWall含Window_01 | WallWindow_400x300 | (200,30,0) | — | — | MI_DevWhite |
|
||||
| | LVL_Z1_STR_房屋SouthWall含Window_02 | WallWindowSmall_200x300 | (600,30,0) | — | — | MI_DevWhite |
|
||||
| | LVL_Z1_STR_房屋WestWall_01 | Wall_400x300 | (200,30,0) | (0,-90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z1_STR_房屋WestWall_02 | Wall_400x100 | (600,30,0) | (0,-90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z1_STR_房屋EastWall_01 | Wall_400x300 | (780,30,0) | (0,90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z1_STR_房屋EastWall_02 | Wall_400x100 | (1180,30,0) | (0,90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z1_STR_房屋NorthWall含Door洞_01 | WallDoor_200x300 | (200,410,0) | (0,180,0) | — | MI_DevWhite |
|
||||
| | LVL_Z1_STR_房屋NorthWall含Door洞_02 | Wall_400x300 | (400,410,0) | (0,180,0) | — | MI_DevWhite |
|
||||
| | LVL_Z1_STR_房屋屋Roof_01 | Floor_300x400 | (190,25,280) | — | — | MI_DevBrown |
|
||||
| | LVL_Z1_STR_房屋屋Roof_02 | Floor_300x400 | (490,25,280) | — | — | MI_DevBrown |
|
||||
| | LVL_Z1_STR_院内Floor板_01 | Floor_1000x1000 | (0,0,0) | — | — | MI_DevGray |
|
||||
|
||||
#### C1.2 布景物(Props)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||
|---|-----------|------|-------------|------|------|------|
|
||||
| | LVL_Z1_PROP_旧自行Car_01 | _composite | (100,650,0) | — | | 靠西院墙停放,后座铁丝筐用 Cube 替代 |
|
||||
| | LVL_Z1_PROP_旧自行Car_02 | Wheel | (100,650,0) | — | | 靠西院墙停放,后座铁丝筐用 Cube 替代 |
|
||||
| | LVL_Z1_PROP_晾衣杆_01 | Beam_300 | (500,600,0) | — | | 挂衣服用 Cube 薄片替代 |
|
||||
| | LVL_Z1_PROP_晾衣杆_02 | Cylinder | (500,600,0) | — | | 挂衣服用 Cube 薄片替代 |
|
||||
| | LVL_Z1_PROP_杂物堆_01 | WoodenBox_Small | (850,100,0) | — | | 东南角 |
|
||||
| | LVL_Z1_PROP_杂物堆_02 | WoodenBox_Small | (890,100,0) | — | | 东南角 |
|
||||
| | LVL_Z1_PROP_杂物堆_03 | Bucket_01 | (930,100,0) | — | | 东南角 |
|
||||
| | LVL_Z1_PROP_Door垫_01 | Cube | (480,740,0) | — | | 院门内侧地面 |
|
||||
| | LVL_Z1_PROP_老式冰Box_01 | Barrel | (720,80,0) | — | | 房屋内东南角,靠东墙 |
|
||||
| | LVL_Z1_PROP_大脑袋电视_01 | Monitor_01 | (320,80,45) | — | | 房屋内西半部,放在书桌上 |
|
||||
| | LVL_Z1_PROP_书Table_01 | SimpleTable_01 | (300,70,0) | — | | 房屋内靠西墙 |
|
||||
| | LVL_Z1_PROP_Bed_01 | BedSingle | (350,250,0) | — | | 房屋内中部偏南 |
|
||||
| | LVL_Z1_PROP_日历_01 | Cube | (500,40,180) | — | | 挂在房屋南墙内侧 |
|
||||
| | LVL_Z1_PROP_电视Cabinet_01 | SimpleTable_02 | (320,55,0) | — | | 电视放在其上(v0.1 可与书桌合并) |
|
||||
|
||||
### C2. Z2 — 小卖部
|
||||
|
||||
#### C2.1 结构体(Structure)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||
|---|-----------|------|-------------|-------------|------|------|
|
||||
| | LVL_Z2_STR_SouthWall_01 | Wall_400x300 | (1000,200,0) | — | — | MI_DevOrange |
|
||||
| | LVL_Z2_STR_SouthWall_02 | Wall_200x300 | (1400,200,0) | — | — | MI_DevOrange |
|
||||
| | LVL_Z2_STR_WestWall_01 | Wall_400x300 | (1000,200,0) | (0,-90,0) | — | MI_DevOrange |
|
||||
| | LVL_Z2_STR_WestWall_02 | Wall_400x100 | (1400,200,0) | (0,-90,0) | — | MI_DevOrange |
|
||||
| | LVL_Z2_STR_EastWall_01 | Wall_400x300 | (1580,200,0) | (0,90,0) | — | MI_DevOrange |
|
||||
| | LVL_Z2_STR_EastWall_02 | Wall_400x100 | (1980,200,0) | (0,90,0) | — | MI_DevOrange |
|
||||
| | LVL_Z2_STR_NorthWall含Door洞_01 | WallDoorGarage_400x400 | (1000,580,0) | (0,180,0) | — | MI_DevOrange |
|
||||
| | LVL_Z2_STR_NorthWall含Door洞_02 | Wall_200x300 | (1000,580,0) | (0,180,0) | — | MI_DevOrange |
|
||||
| | LVL_Z2_STR_屋Roof_01 | Floor_300x400 | (990,190,280) | — | — | MI_DevBrown |
|
||||
| | LVL_Z2_STR_屋Roof_02 | Floor_300x400 | (1290,190,280) | — | — | MI_DevBrown |
|
||||
| | LVL_Z2_STR_Floor板_01 | Floor_1000x1000 | (1000,200,0) | — | — | MI_DevGray |
|
||||
| | LVL_Z2_STR_遮阳棚前廊Roof_01 | Floor_200x300 | (1000,600,260) | — | — | MI_DevWhite |
|
||||
| | LVL_Z2_STR_遮阳棚前廊Roof_02 | Floor_200x300 | (1200,600,260) | — | — | MI_DevWhite |
|
||||
|
||||
#### C2.2 布景物(Props)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||
|---|-----------|------|-------------|------|------|------|
|
||||
| | LVL_Z2_PROP_玻璃CabinetCounter_01 | Cube | (1200,500,0) | — | | 店内中偏北,面向门口 |
|
||||
| | LVL_Z2_PROP_玻璃CabinetCounter_02 | MI_DevGlassWhite | (1300,500,0) | — | | 店内中偏北,面向门口 |
|
||||
| | LVL_Z2_PROP_零食货Shelf靠EastWall_01 | Fence_Small_400 | (1520,300,0) | — | | 东墙内侧,多层货架 |
|
||||
| | LVL_Z2_PROP_零食货Shelf靠EastWall_02 | Fence_Small_400 | (1920,300,0) | — | | 东墙内侧,多层货架 |
|
||||
| | LVL_Z2_PROP_零食货Shelf靠EastWall_03 | MI_DevGray | (2320,300,0) | — | | 东墙内侧,多层货架 |
|
||||
| | LVL_Z2_PROP_零食货Shelf靠SouthWall_01 | Fence_Small_400 | (1100,230,0) | — | | 南墙内侧 |
|
||||
| | LVL_Z2_PROP_零食货Shelf靠SouthWall_02 | MI_DevGray | (1500,230,0) | — | | 南墙内侧 |
|
||||
| | LVL_Z2_PROP_老式冰Cabinet_01 | Cube | (1050,400,0) | — | | 西南角,顶盖掀开式 |
|
||||
| | LVL_Z2_PROP_老式冰Cabinet_02 | MI_DevWhite | (1150,400,0) | — | | 西南角,顶盖掀开式 |
|
||||
| | LVL_Z2_PROP_小电视_01 | Monitor_01 | (1350,540,90) | — | | 放在柜台角落 |
|
||||
| | LVL_Z2_PROP_招牌_01 | Cube | (1150,610,280) | — | | 北墙外侧上方,蓝底白字"百货" |
|
||||
| | LVL_Z2_PROP_招牌_02 | MI_DevBlue | (1250,610,280) | — | | 北墙外侧上方,蓝底白字"百货" |
|
||||
| | LVL_Z2_PROP_卡片Shelf_01 | Cube | (1400,550,0) | — | | 柜台旁地面 |
|
||||
|
||||
### C3. Z3 — 主街道
|
||||
|
||||
#### C3.2 布景物(Props)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||
|---|-----------|------|-------------|------|------|------|
|
||||
| | LVL_Z3_PROP_公用电话亭_01 | Cube | (1800,1180,0) | — | | 街道北侧靠墙 |
|
||||
| | LVL_Z3_PROP_公用电话亭_02 | Cylinder | (1900,1180,0) | — | | 街道北侧靠墙 |
|
||||
| | LVL_Z3_PROP_老式自行Car_01 | Car01 | (2800,880,0) | (0.50,0.50,0.50) | | 街道南侧靠墙停放 |
|
||||
| | LVL_Z3_PROP_晾衣杆_01 | Beam_300 | (1200,1250,0) | — | | 街道北侧,伸出墙面的晾衣杆 |
|
||||
| | LVL_Z3_PROP_晾衣杆_02 | Cylinder | (1200,1250,0) | — | | 街道北侧,伸出墙面的晾衣杆 |
|
||||
| | LVL_Z3_PROP_Bottom水道盖_01 | CirclePlatform | (800,1050,0) | — | | 街道路面中央偏西 |
|
||||
| | LVL_Z3_PROP_WallTop涂鸦区域_01 | Cube | (3200,1270,100) | — | | 北墙 C 段墙面中部 |
|
||||
| | LVL_Z3_PROP_WallTop涂鸦区域_02 | MI_DevYellow | (3300,1270,100) | — | | 北墙 C 段墙面中部 |
|
||||
| | LVL_Z3_PROP_广告Wall_01 | DisplayPanels_400x200 | (3800,1270,0) | — | | 北墙 D 段起始处,手绘广告 |
|
||||
| | LVL_Z3_PROP_垃圾Barrel_01 | TrashCans_01 | (4200,880,0) | — | | 街道南侧 |
|
||||
| | LVL_Z3_PROP_电线杆West_01 | Cylinder | (1000,1050,0) | — | | 街道中轴 |
|
||||
| | LVL_Z3_PROP_电线杆East_01 | Cylinder | (3500,1050,0) | — | | 街道中轴 |
|
||||
|
||||
### C4. Z4 — 空地
|
||||
|
||||
#### C4.1 结构体(Structure)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||
|---|-----------|------|-------------|-------------|------|------|
|
||||
| | LVL_Z4_STR_Floor面_01 | Cube | (3500,1300,0) | — | — | MI_DevBrown |
|
||||
| | LVL_Z4_STR_East边界Wall挡_01 | Cube | (4980,1300,0) | (0,90,0) | — | MI_DevBlue |
|
||||
| | LVL_Z4_STR_North边界Wall挡_01 | Cube | (3500,2480,0) | (0,180,0) | — | MI_DevBlue |
|
||||
| | LVL_Z4_STR_West边界矮Wall_01 | Cube | (3500,1300,0) | (0,-90,0) | — | MI_DevGray |
|
||||
|
||||
#### C4.2 布景物(Props)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||
|---|-----------|------|-------------|------|------|------|
|
||||
| | LVL_Z4_PROP_大梧桐Tree_01 | Tree_01 | (4250,1900,0) | — | | 空地偏北中央 |
|
||||
| | LVL_Z4_PROP_散落砖块_01 | WoodenBox_Small | (3700,1600,0) | — | | 空地西半部 |
|
||||
| | LVL_Z4_PROP_散落砖块_02 | WoodenBox_Small | (3708,1600,0) | — | | 空地西半部 |
|
||||
| | LVL_Z4_PROP_散落砖块_03 | WoodenBox_Small | (3716,1600,0) | — | | 空地西半部 |
|
||||
| | LVL_Z4_PROP_散落砖块_04 | WoodenBox_Small | (3724,1600,0) | — | | 空地西半部 |
|
||||
| | LVL_Z4_PROP_散落砖块_05 | WoodenBox_Small | (3732,1600,0) | — | | 空地西半部 |
|
||||
| | LVL_Z4_PROP_积水坑_01 | Cube | (4600,1500,-5) | — | | 空地东南角,嵌入地面 |
|
||||
| | LVL_Z4_PROP_积水坑_02 | MI_DevGlassBlue | (4700,1500,-5) | — | | 空地东南角,嵌入地面 |
|
||||
| | LVL_Z4_PROP_弹珠圈痕迹_01 | CirclePlatform | (4100,1800,0) | — | | 空地中偏北,地面上的圆圈 |
|
||||
| | LVL_Z4_PROP_弹珠圈痕迹_02 | MI_DevGray | (4100,1800,0) | — | | 空地中偏北,地面上的圆圈 |
|
||||
| | LVL_Z4_PROP_废弃建材_01 | WoodenPallet_01 | (4800,2300,0) | — | | 空地东北角 |
|
||||
| | LVL_Z4_PROP_废弃建材_02 | WoodenBox_Normal | (4800,2300,0) | — | | 空地东北角 |
|
||||
|
||||
### C5. Z5 — 巷子
|
||||
|
||||
#### C5.1 结构体(Structure)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||
|---|-----------|------|-------------|-------------|------|------|
|
||||
| | LVL_Z5_STR_WestWall_01 | Wall_400x300 | (2300,1300,0) | (0,-90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z5_STR_WestWall_02 | Wall_400x300 | (2700,1300,0) | (0,-90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z5_STR_WestWall_03 | Wall_400x300 | (3100,1300,0) | (0,-90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z5_STR_WestWall_04 | Wall_400x300 | (3500,1300,0) | (0,-90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z5_STR_WestWall_05 | Wall_400x300 | (3900,1300,0) | (0,-90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z5_STR_EastWall_01 | Wall_400x300 | (2580,1300,0) | (0,90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z5_STR_EastWall_02 | Wall_400x300 | (2980,1300,0) | (0,90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z5_STR_EastWall_03 | Wall_400x300 | (3380,1300,0) | (0,90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z5_STR_EastWall_04 | Wall_400x300 | (3780,1300,0) | (0,90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z5_STR_EastWall_05 | Wall_400x300 | (4180,1300,0) | (0,90,0) | — | MI_DevWhite |
|
||||
| | LVL_Z5_STR_Floor面_01 | Floor_1000x1000 | (2300,1300,0) | — | — | MI_DevGray |
|
||||
| | LVL_Z5_STR_Floor面_02 | Floor_1000x1000 | (3300,1300,0) | — | — | MI_DevGray |
|
||||
|
||||
#### C5.2 布景物(Props)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||
|---|-----------|------|-------------|------|------|------|
|
||||
| | LVL_Z5_PROP_废纸Box_01 | WoodenBox_Small | (2380,2900,0) | — | | 巷子深处(北段)靠西墙 |
|
||||
| | LVL_Z5_PROP_蜘蛛网_01 | Cube | (2305,2300,250) | — | | 西墙高处 |
|
||||
| | LVL_Z5_PROP_老Window户WestWall_01 | FakeWindowsFrame | (2310,2100,150) | — | | 西墙中段,不可交互 |
|
||||
| | LVL_Z5_PROP_花盆WindowCounter_01 | Cylinder | (2340,2100,150) | — | | 窗台上的花盆 |
|
||||
| | LVL_Z5_PROP_青苔Floor面细节_01 | Cube | (2400,2700,0) | — | | 巷子北段地面 |
|
||||
| | LVL_Z5_PROP_青苔Floor面细节_02 | MI_DevFoliage | (2500,2700,0) | — | | 巷子北段地面 |
|
||||
|
||||
### C6. Z6 — 水塘边
|
||||
|
||||
#### C6.1 结构体(Structure)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||
|---|-----------|------|-------------|-------------|------|------|
|
||||
| | LVL_Z6_STR_水面_01 | Floor_1000x1000 | (1750,3650,-20) | — | — | MI_DevSkyBlue |
|
||||
| | LVL_Z6_STR_水面_02 | Floor_200x300 | (2750,3650,-20) | — | — | MI_DevSkyBlue |
|
||||
| | LVL_Z6_STR_岸边Floor面South_01 | Floor_1000x1000 | (1600,3300,0) | — | — | MI_DevBrown |
|
||||
| | LVL_Z6_STR_岸边Floor面South_02 | Floor_200x300 | (2600,3300,0) | — | — | MI_DevBrown |
|
||||
| | LVL_Z6_STR_岸边Floor面East_01 | Floor_1000x1000 | (2950,3650,0) | (0,90,0) | — | MI_DevBrown |
|
||||
| | LVL_Z6_STR_岸边Floor面West_01 | Floor_1000x1000 | (1600,3650,0) | (0,-90,0) | — | MI_DevBrown |
|
||||
| | LVL_Z6_STR_East边界Wall挡_01 | BarrierWall_600 | (3080,3300,0) | (0,90,0) | — | MI_DevBlue |
|
||||
| | LVL_Z6_STR_East边界Wall挡_02 | BarrierWall_600 | (3680,3300,0) | (0,90,0) | — | MI_DevBlue |
|
||||
| | LVL_Z6_STR_North边界Wall挡_01 | BarrierWall_600 | (1600,4680,0) | (0,180,0) | — | MI_DevBlue |
|
||||
| | LVL_Z6_STR_North边界Wall挡_02 | BarrierWall_600 | (2200,4680,0) | (0,180,0) | — | MI_DevBlue |
|
||||
| | LVL_Z6_STR_North边界Wall挡_03 | BarrierWall_200 | (2800,4680,0) | (0,180,0) | — | MI_DevBlue |
|
||||
|
||||
#### C6.2 布景物(Props)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||
|---|-----------|------|-------------|------|------|------|
|
||||
| | LVL_Z6_PROP_芦苇丛South岸Left_01 | Cylinder | (1800,3600,0) | — | | 水塘南岸边 |
|
||||
| | LVL_Z6_PROP_芦苇丛South岸Left_02 | Cylinder | (1900,3600,0) | — | | 水塘南岸边 |
|
||||
| | LVL_Z6_PROP_芦苇丛South岸Left_03 | Cylinder | (2000,3600,0) | — | | 水塘南岸边 |
|
||||
| | LVL_Z6_PROP_芦苇丛South岸Left_04 | Cylinder | (2100,3600,0) | — | | 水塘南岸边 |
|
||||
| | LVL_Z6_PROP_芦苇丛South岸Left_05 | Cylinder | (2200,3600,0) | — | | 水塘南岸边 |
|
||||
| | LVL_Z6_PROP_芦苇丛South岸Left_06 | MI_DevFoliage | (2300,3600,0) | — | | 水塘南岸边 |
|
||||
| | LVL_Z6_PROP_芦苇丛South岸Right_01 | Cylinder | (2700,3600,0) | — | | 水塘南岸边 |
|
||||
| | LVL_Z6_PROP_芦苇丛South岸Right_02 | Cylinder | (2800,3600,0) | — | | 水塘南岸边 |
|
||||
| | LVL_Z6_PROP_芦苇丛South岸Right_03 | Cylinder | (2900,3600,0) | — | | 水塘南岸边 |
|
||||
| | LVL_Z6_PROP_芦苇丛South岸Right_04 | Cylinder | (3000,3600,0) | — | | 水塘南岸边 |
|
||||
| | LVL_Z6_PROP_芦苇丛South岸Right_05 | Cylinder | (3100,3600,0) | — | | 水塘南岸边 |
|
||||
| | LVL_Z6_PROP_芦苇丛South岸Right_06 | MI_DevFoliage | (3200,3600,0) | — | | 水塘南岸边 |
|
||||
| | LVL_Z6_PROP_芦苇丛East岸_01 | Cylinder | (2900,4100,0) | — | | |
|
||||
| | LVL_Z6_PROP_芦苇丛East岸_02 | Cylinder | (3000,4100,0) | — | | |
|
||||
| | LVL_Z6_PROP_芦苇丛East岸_03 | Cylinder | (3100,4100,0) | — | | |
|
||||
| | LVL_Z6_PROP_芦苇丛East岸_04 | MI_DevFoliage | (3200,4100,0) | — | | |
|
||||
| | LVL_Z6_PROP_荷叶_01 | CirclePlatform | (2200,3900,-18) | — | | 水面上 |
|
||||
| | LVL_Z6_PROP_荷叶_02 | CirclePlatform | (2207,3900,-18) | — | | 水面上 |
|
||||
| | LVL_Z6_PROP_荷叶_03 | CirclePlatform | (2215,3900,-18) | — | | 水面上 |
|
||||
| | LVL_Z6_PROP_荷叶_04 | CirclePlatform | (2222,3900,-18) | — | | 水面上 |
|
||||
| | LVL_Z6_PROP_荷叶_05 | MI_DevGreen | (2230,3900,-18) | — | | 水面上 |
|
||||
| | LVL_Z6_PROP_青蛙_01 | Cube | (2100,3680,0) | — | | 南岸石头旁 |
|
||||
| | LVL_Z6_PROP_青蛙_02 | MI_DevGreen | (2200,3680,0) | — | | 南岸石头旁 |
|
||||
|
||||
### C7. Z7 — 老居民楼
|
||||
|
||||
#### C7.1 结构体(Structure)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |
|
||||
|---|-----------|------|-------------|-------------|------|------|
|
||||
| | LVL_Z7_STR_居民楼主体_01 | Building_01 | (5800,200,0) | — | — | MI_DevGray |
|
||||
| | LVL_Z7_STR_碰撞WallWall_01 | Wall_400x300 | (5750,150,0) | — | — | MI_DevGray |
|
||||
| | LVL_Z7_STR_碰撞WallWall_02 | Wall_400x300 | (6150,150,0) | — | — | MI_DevGray |
|
||||
| | LVL_Z7_STR_碰撞WallWall_03 | Wall_400x300 | (6550,150,0) | — | — | MI_DevGray |
|
||||
| | LVL_Z7_STR_碰撞WallWall_04 | Wall_100x300 | (6950,150,0) | — | — | MI_DevGray |
|
||||
| | LVL_Z7_STR_碰撞WallWall侧_01 | Wall_400x300 | (5750,150,0) | — | — | MI_DevGray |
|
||||
| | LVL_Z7_STR_碰撞WallWall侧_02 | Wall_400x300 | (6150,150,0) | — | — | MI_DevGray |
|
||||
|
||||
#### C7.2 布景物(Props)
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||
|---|-----------|------|-------------|------|------|------|
|
||||
| | LVL_Z7_PROP_晾衣杆阳Counter_01 | Beam_300 | (6100,190,800) | — | | 不同楼层阳台外伸 |
|
||||
| | LVL_Z7_PROP_晾衣杆阳Counter_02 | Beam_300 | (6133,190,800) | — | | 不同楼层阳台外伸 |
|
||||
| | LVL_Z7_PROP_晾衣杆阳Counter_03 | Beam_300 | (6166,190,800) | — | | 不同楼层阳台外伸 |
|
||||
| | LVL_Z7_PROP_垃圾Barrel_01 | TrashCans_01 | (6000,140,0) | — | | 楼下地面 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 四、Phase C — 全局物件
|
||||
|
||||
| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |
|
||||
|---|-----------|------|-------------|------|------|------|
|
||||
| | LVL_Global_PROP_路Lamp_01 | RoadLamp_01 | (1500,1050,0) | — | | 街道中轴线偏南 |
|
||||
| | LVL_Global_PROP_路Lamp_02 | RoadLamp_01 | (1500,1050,0) | — | | 街道中轴线偏南 |
|
||||
| | LVL_Global_PROP_路Lamp_03 | RoadLamp_01 | (1500,1050,0) | — | | 街道中轴线偏南 |
|
||||
| | LVL_Global_PROP_垃圾Barrel_01 | TrashCans_01 | (2000,850,0) | — | | 街道南侧靠墙 |
|
||||
| | LVL_Global_PROP_垃圾Barrel_01 | MI_DevGreen | (2000,850,0) | — | | 街道南侧靠墙 |
|
||||
| | LVL_Global_PROP_垃圾Barrel_02 | TrashCans_01 | (2000,850,0) | — | | 街道南侧靠墙 |
|
||||
| | LVL_Global_PROP_垃圾Barrel_02 | MI_DevGreen | (2000,850,0) | — | | 街道南侧靠墙 |
|
||||
| | LVL_Global_PROP_电线杆_01 | Cylinder | (1000,1050,0) | — | | 街道中轴,与路灯交错 |
|
||||
| | LVL_Global_PROP_电线杆_02 | Cylinder | (1000,1050,0) | — | | 街道中轴,与路灯交错 |
|
||||
| | LVL_Global_PROP_施工Wall挡South边界补充_01 | BarrierWall_600 | (0,0,0) | — | | 街道南侧背后的南场景边界 |
|
||||
| | LVL_Global_PROP_施工Wall挡South边界补充_02 | MI_DevBlue | (0,0,0) | — | | 街道南侧背后的南场景边界 |
|
||||
|
||||
---
|
||||
|
||||
## 五、Phase D — 光照
|
||||
|
||||
### D1. 定向光(Directional Light)
|
||||
|
||||
| 类型 | 位置/方向 | 参数 |
|
||||
|------|---------|------|
|
||||
| Directional Light | 从西南方向照射,仰角约 45°(建议值,可调,范围 30°-60°) | 色温:暖白 ~5500K(建议值,可调);强度:中等(建议值,可调) |
|
||||
> 夏日午后主光源
|
||||
|
||||
### D2. 天空光(Sky Light)
|
||||
|
||||
| 类型 | 位置/方向 | 参数 |
|
||||
|------|---------|------|
|
||||
| Sky Light | 全局 | 强度:低-中(建议值,可调) |
|
||||
> 环境补光,避免纯黑阴影
|
||||
|
||||
### D3. 路灯点光源
|
||||
|
||||
> v0.1 为装饰性光源,午后光照下几乎不可见,数量: 3
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 六、Phase E — 验证清单
|
||||
|
||||
| # | 检查项 | 状态 |
|
||||
|---|--------|------|
|
||||
| 1 | 场景尺寸 8000×6000cm 正确 | ⬜ |
|
||||
| 2 | 7 个区域均已搭建 | ⬜ |
|
||||
| 3 | 所有边界围挡无缺口 | ⬜ |
|
||||
| 4 | 玩家起点可到达 | ⬜ |
|
||||
| 5 | 所有区域可通行 | ⬜ |
|
||||
| 6 | 建筑入口与流线对齐 | ⬜ |
|
||||
| 7 | 光照方向与场景一致 | ⬜ |
|
||||
| 8 | Actor 总数: 161 | ⬜ |
|
||||
|
||||
---
|
||||
|
||||
> 搭建指南生成完成。共 7 个区域,161 个 Actor。
|
||||
@@ -0,0 +1,52 @@
|
||||
# execution-to-guide
|
||||
|
||||
关卡执行案 → 搭建指南 转换器。
|
||||
|
||||
将 `docs/design/关卡-vXX-<名称>-执行案.md` 自动转换为 `docs/technical/关卡-vXX-搭建指南.md`,供 `level-builder` 工具解析并在 UE5 中搭建白模关卡。
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
# 基本用法(自动推断输出路径到 docs/technical/)
|
||||
cd tools/execution-to-guide
|
||||
python converter.py -i ../../docs/design/关卡-v01-城郊小街-执行案.md
|
||||
|
||||
# 指定输出路径
|
||||
python converter.py -i ../../docs/design/关卡-v01-城郊小街-执行案.md -o ../../docs/technical/关卡-v01-搭建指南.md
|
||||
|
||||
# 详细日志
|
||||
python converter.py -i <执行案.md> -v
|
||||
```
|
||||
|
||||
## 依赖
|
||||
|
||||
- `tools/level-builder/config/` 下的 `asset_mapping.py`、`material_mapping.py`、`substitution_rules.py`
|
||||
- `tools/level-builder/parsers/markdown_table.py`
|
||||
|
||||
工具会自动从上级 `level-builder` 目录加载这些依赖。
|
||||
|
||||
## 模块说明
|
||||
|
||||
| 文件 | 功能 |
|
||||
|------|------|
|
||||
| `converter.py` | 主入口,编排解析→展开→生成流程 |
|
||||
| `execution_parser.py` | 解析执行案 Markdown,提取区域/结构体/布景/组合资产/边界/光照 |
|
||||
| `asset_expander.py` | 将 BSP 资产建议字符串展开为 Actor 规格(坐标计算、旋转推断、资产映射) |
|
||||
| `guide_writer.py` | 生成搭建指南 Markdown(匹配 `build_guide_parser.py` 的格式预期) |
|
||||
| `composite_writer.py` | 生成组合资产清单 + JSON 模板(无直接 BSP 匹配的物件) |
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
关卡执行案 (.md)
|
||||
│
|
||||
▼
|
||||
execution_parser.py → ExecutionPlanData
|
||||
│
|
||||
▼
|
||||
asset_expander.py → list[ActorSpec] (展开 BSP 建议 + 坐标计算)
|
||||
│
|
||||
▼
|
||||
guide_writer.py → 搭建指南 (.md) (精确匹配 build_guide_parser 格式)
|
||||
composite_writer.py → 组合资产清单 (.md + .json)
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
# execution-to-guide 工具包
|
||||
@@ -0,0 +1,423 @@
|
||||
"""BSP 资产建议展开器。
|
||||
|
||||
将执行案中的"BSP 资产建议"字符串拆分为可放置的 Actor 规格。
|
||||
|
||||
核心转换:
|
||||
- "Wall_400x300 × 2" → 2 个 Actor,间隔 400cm
|
||||
- "Wall_400x300 + Wall_200x300" → 2 种资产各 1 个
|
||||
- "Cube 缩放(0.3,0.3,1.6)" → 1 个 Cube,带缩放参数
|
||||
- 区域原点 + 相对偏移 → 绝对坐标
|
||||
- 构件名称/方位 → 旋转推断
|
||||
|
||||
同时复用 level-builder 的 asset_mapping / material_mapping / substitution_rules。
|
||||
"""
|
||||
|
||||
import re
|
||||
import math
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
# 允许从包内或直接运行
|
||||
from pathlib import Path
|
||||
try:
|
||||
from config.asset_mapping import MESH_ASSETS, MESH_ROOT, get_pivot_compensation
|
||||
from config.material_mapping import MATERIAL_PATHS
|
||||
from config.substitution_rules import SUBSTITUTION_RULES
|
||||
except ImportError:
|
||||
import sys
|
||||
_lb = str(Path(__file__).resolve().parent.parent / "level-builder")
|
||||
if _lb not in sys.path:
|
||||
sys.path.insert(0, _lb)
|
||||
from config.asset_mapping import MESH_ASSETS, MESH_ROOT, get_pivot_compensation
|
||||
from config.material_mapping import MATERIAL_PATHS
|
||||
from config.substitution_rules import SUBSTITUTION_RULES
|
||||
|
||||
logger = logging.getLogger("execution_to_guide.expander")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 数据结构
|
||||
# ============================================================
|
||||
|
||||
@dataclass
|
||||
class ActorSpec:
|
||||
"""展开后的单个 Actor 规格。"""
|
||||
actor_name: str # LVL_Z1_STR_SouthWall_01
|
||||
asset_key: str # Wall_400x300
|
||||
asset_path: str # UE5 资产路径
|
||||
location: tuple # (X, Y, Z) 绝对坐标 cm
|
||||
rotation: tuple # (Pitch, Yaw, Roll) 度
|
||||
scale: tuple # (X, Y, Z) 缩放
|
||||
material: str # MI_DevRed
|
||||
material_path: str # UE5 材质路径
|
||||
category: str # STR | PROP | COMP
|
||||
zone_id: str # Z1 | GLOBAL
|
||||
folder: str # Outliner 文件夹路径
|
||||
note: str = ""
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 方向→旋转映射
|
||||
# ============================================================
|
||||
|
||||
# 从构件名称中的方位词推断旋转
|
||||
# UE5: Yaw 绕 Z 轴旋转,沿 X 正方向为 0°
|
||||
# 南墙(沿 X 轴)= Yaw 0°,东墙(沿 Y 轴)= Yaw 90°
|
||||
DIRECTION_YAW = {
|
||||
"南": 0.0, # 沿 X 轴,面朝南(-Y)
|
||||
"东": 90.0, # 沿 Y 轴,面朝东(+X)
|
||||
"西": -90.0, # 沿 Y 轴,面朝西(-X)
|
||||
"北": 180.0, # 沿 X 轴,面朝北(+Y)
|
||||
"左": 90.0,
|
||||
"右": -90.0,
|
||||
"上": 0.0,
|
||||
"下": 0.0,
|
||||
}
|
||||
|
||||
|
||||
def infer_rotation_from_name(name: str) -> tuple:
|
||||
"""从构件名称推断旋转。
|
||||
|
||||
查找名称中的方位词(南/东/西/北),返回对应的 (Pitch, Yaw, Roll)。
|
||||
如果没有方位词,返回 (0, 0, 0)。
|
||||
"""
|
||||
for direction, yaw in DIRECTION_YAW.items():
|
||||
if direction in name:
|
||||
return (0.0, yaw, 0.0)
|
||||
return (0.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def infer_rotation_from_offset(offset: tuple, zone_size: tuple) -> tuple:
|
||||
"""从偏移位置推断旋转(备用方案)。
|
||||
|
||||
根据物件在区域中的位置判断朝向:
|
||||
- 偏移 Y ≈ 0 → 南墙,Y ≈ max → 北墙
|
||||
- 偏移 X ≈ 0 → 西墙,X ≈ max → 东墙
|
||||
"""
|
||||
dx, dy, dz = offset
|
||||
w, d = zone_size[0], zone_size[1] if len(zone_size) > 1 else 0
|
||||
|
||||
if w > 0 and d > 0:
|
||||
# 判断更接近哪条边
|
||||
x_ratio = dx / w if w else 0
|
||||
y_ratio = dy / d if d else 0
|
||||
|
||||
if y_ratio < 0.1:
|
||||
return (0.0, 0.0, 0.0) # 南
|
||||
elif y_ratio > 0.9:
|
||||
return (0.0, 180.0, 0.0) # 北
|
||||
elif x_ratio < 0.1:
|
||||
return (0.0, -90.0, 0.0) # 西
|
||||
elif x_ratio > 0.9:
|
||||
return (0.0, 90.0, 0.0) # 东
|
||||
|
||||
return (0.0, 0.0, 0.0)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# BSP 建议解析
|
||||
# ============================================================
|
||||
|
||||
def parse_bsp_suggestion(bsp_text: str) -> list[dict]:
|
||||
"""解析 BSP 资产建议字符串为资产列表。
|
||||
|
||||
支持的格式:
|
||||
- "Wall_400x300" → [{"key": "Wall_400x300", "count": 1}]
|
||||
- "Wall_400x300 × 2" → [{"key": "Wall_400x300", "count": 2}]
|
||||
- "Wall_400x300 × 2 + Wall_200x300" → [{"key": "Wall_400x300", "count": 2}, {"key": "Wall_200x300", "count": 1}]
|
||||
- "Cube 缩放(0.3,0.3,1.6)" → [{"key": "Cube", "count": 1, "scale": (0.3,0.3,1.6)}]
|
||||
- "Car01 缩放50%" → [{"key": "Car01", "count": 1, "scale": (0.5,0.5,0.5)}]
|
||||
- "Floor_1000x1000 缩放(8,6,1)" → [{"key": "Floor_1000x1000", "count": 1, "scale": (8,6,1)}]
|
||||
- "Primitives 组合" → [{"key": "_composite", "count": 1}]
|
||||
- "BarrierWall_600" → [{"key": "BarrierWall_600", "count": 1}]
|
||||
|
||||
Returns:
|
||||
list of dicts, each with:
|
||||
key: asset key string
|
||||
count: int
|
||||
scale: optional tuple (X, Y, Z)
|
||||
"""
|
||||
if not bsp_text or not bsp_text.strip():
|
||||
return []
|
||||
|
||||
bsp_text = bsp_text.strip()
|
||||
results = []
|
||||
|
||||
# 按 + 分割多个资产段
|
||||
segments = re.split(r'\s*\+\s*', bsp_text)
|
||||
|
||||
for seg in segments:
|
||||
seg = seg.strip()
|
||||
if not seg:
|
||||
continue
|
||||
|
||||
# 检测 Primitives 组合标记
|
||||
if "Primitives 组合" in seg or "组合" in seg and not re.search(r'[A-Z]', seg):
|
||||
results.append({"key": "_composite", "count": 1})
|
||||
continue
|
||||
|
||||
# 提取资产 key
|
||||
key_match = re.match(r'([A-Za-z_]\w*(?:\d+x\d+)?)', seg)
|
||||
if not key_match:
|
||||
continue
|
||||
|
||||
asset_key = key_match.group(1)
|
||||
remaining = seg[key_match.end():]
|
||||
|
||||
# 提取数量:× 2 或 *2 或 x2
|
||||
count = 1
|
||||
count_match = re.search(r'[×x*]\s*(\d+)', remaining)
|
||||
if count_match:
|
||||
count = int(count_match.group(1))
|
||||
|
||||
# 提取缩放:缩放(X,Y,Z) 或 缩放NN%
|
||||
scale = None
|
||||
scale_match = re.search(r'缩放\s*\(([\d.]+)\s*[,,]\s*([\d.]+)\s*[,,]\s*([\d.]+)\)', remaining)
|
||||
if scale_match:
|
||||
scale = (float(scale_match.group(1)), float(scale_match.group(2)), float(scale_match.group(3)))
|
||||
else:
|
||||
pct_match = re.search(r'缩放\s*(\d+)%', remaining)
|
||||
if pct_match:
|
||||
s = int(pct_match.group(1)) / 100.0
|
||||
scale = (s, s, s)
|
||||
|
||||
results.append({"key": asset_key, "count": count, "scale": scale})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 资产展开
|
||||
# ============================================================
|
||||
|
||||
class AssetExpander:
|
||||
"""将执行案条目展开为 Actor 规格。"""
|
||||
|
||||
def __init__(self, zones: list):
|
||||
"""
|
||||
Args:
|
||||
zones: list[ZoneInfo] 区域信息列表
|
||||
"""
|
||||
self.zones = {z.zone_id: z for z in zones}
|
||||
self.actor_counters = {} # zone_id -> {category: counter}
|
||||
|
||||
def expand_structure(self, item, zone_id: str) -> list[ActorSpec]:
|
||||
"""展开结构体条目。
|
||||
|
||||
Args:
|
||||
item: StructureItem
|
||||
zone_id: 区域 ID
|
||||
|
||||
Returns:
|
||||
展开后的 ActorSpec 列表
|
||||
"""
|
||||
zone = self.zones.get(zone_id)
|
||||
if not zone:
|
||||
logger.warning(f"未找到区域 {zone_id},跳过结构体 {item.item_id}")
|
||||
return []
|
||||
|
||||
return self._expand_item(
|
||||
item_id=item.item_id,
|
||||
name=item.name,
|
||||
size=item.size,
|
||||
offset=item.offset,
|
||||
bsp_suggestion=item.bsp_suggestion,
|
||||
tag_color=item.tag_color,
|
||||
zone_id=zone_id,
|
||||
zone=zone,
|
||||
category="STR",
|
||||
note="",
|
||||
)
|
||||
|
||||
def expand_prop(self, item, zone_id: str) -> list[ActorSpec]:
|
||||
"""展开布景物条目。"""
|
||||
zone = self.zones.get(zone_id)
|
||||
if not zone:
|
||||
logger.warning(f"未找到区域 {zone_id},跳过布景 {item.item_id}")
|
||||
return []
|
||||
|
||||
return self._expand_item(
|
||||
item_id=item.item_id,
|
||||
name=item.name,
|
||||
size=item.size,
|
||||
offset=item.offset,
|
||||
bsp_suggestion=item.bsp_suggestion,
|
||||
tag_color="",
|
||||
zone_id=zone_id,
|
||||
zone=zone,
|
||||
category="PROP",
|
||||
note=item.note,
|
||||
)
|
||||
|
||||
def expand_global(self, item) -> list[ActorSpec]:
|
||||
"""展开全局物件条目。全局物件使用绝对坐标,不需要区域原点。"""
|
||||
parts = parse_bsp_suggestion(item.bsp_suggestion)
|
||||
if not parts:
|
||||
# 尝试 substitution_rules
|
||||
sub = SUBSTITUTION_RULES.get(item.name)
|
||||
if sub:
|
||||
parts = [{"key": sub["asset"], "count": item.count, "scale": sub.get("scale")}]
|
||||
else:
|
||||
parts = [{"key": "Cube", "count": item.count, "scale": None}]
|
||||
|
||||
actors = []
|
||||
for i in range(item.count):
|
||||
for part in parts:
|
||||
asset_key = part["key"]
|
||||
if asset_key == "_composite":
|
||||
continue
|
||||
|
||||
actor_name = f"LVL_Global_PROP_{self._slugify(item.name)}"
|
||||
if item.count > 1:
|
||||
actor_name += f"_{i + 1:02d}"
|
||||
elif len(parts) > 1:
|
||||
actor_name += f"_{parts.index(part) + 1:02d}"
|
||||
|
||||
scale = part.get("scale")
|
||||
if scale is None:
|
||||
scale = (1.0, 1.0, 1.0)
|
||||
|
||||
asset_path = MESH_ASSETS.get(asset_key, {}).get("path", "")
|
||||
material = ""
|
||||
mat_path = ""
|
||||
|
||||
actors.append(ActorSpec(
|
||||
actor_name=actor_name,
|
||||
asset_key=asset_key,
|
||||
asset_path=asset_path,
|
||||
location=item.location,
|
||||
rotation=(0.0, 0.0, 0.0),
|
||||
scale=scale,
|
||||
material=material,
|
||||
material_path=mat_path,
|
||||
category="PROP",
|
||||
zone_id="GLOBAL",
|
||||
folder=self._get_folder("GLOBAL", "PROP"),
|
||||
note=item.note,
|
||||
))
|
||||
|
||||
return actors
|
||||
|
||||
def _expand_item(self, item_id: str, name: str, size: tuple, offset: tuple,
|
||||
bsp_suggestion: str, tag_color: str, zone_id: str,
|
||||
zone, category: str, note: str) -> list[ActorSpec]:
|
||||
"""通用展开逻辑。"""
|
||||
parts = parse_bsp_suggestion(bsp_suggestion)
|
||||
if not parts:
|
||||
# 尝试 substitution_rules
|
||||
sub = SUBSTITUTION_RULES.get(name)
|
||||
if sub:
|
||||
parts = [{"key": sub["asset"], "count": 1, "scale": sub.get("scale")}]
|
||||
if not tag_color and sub.get("material"):
|
||||
tag_color = sub["material"]
|
||||
else:
|
||||
parts = [{"key": "Cube", "count": 1, "scale": None}]
|
||||
|
||||
actors = []
|
||||
# 跟踪资产段的累积偏移(用于 × N 的资产沿主轴排列)
|
||||
cum_offset_x = 0.0
|
||||
|
||||
for part in parts:
|
||||
asset_key = part["key"]
|
||||
count = part.get("count", 1)
|
||||
part_scale = part.get("scale")
|
||||
|
||||
for idx in range(count):
|
||||
# 计算绝对位置
|
||||
abs_x = zone.origin[0] + offset[0] + cum_offset_x
|
||||
abs_y = zone.origin[1] + offset[1]
|
||||
abs_z = zone.origin[2] + offset[2]
|
||||
|
||||
# 确定缩放
|
||||
if part_scale:
|
||||
scale = part_scale
|
||||
else:
|
||||
scale = (1.0, 1.0, 1.0)
|
||||
|
||||
# 确定旋转
|
||||
rotation = infer_rotation_from_name(name)
|
||||
|
||||
# 确定资产路径
|
||||
asset_info = MESH_ASSETS.get(asset_key, {})
|
||||
asset_path = asset_info.get("path", "")
|
||||
|
||||
# 确定材质
|
||||
material = tag_color
|
||||
mat_path = MATERIAL_PATHS.get(material, "") if material else ""
|
||||
|
||||
# 生成 Actor 名称
|
||||
actor_name = self._make_actor_name(zone_id, category, name, len(actors) + 1)
|
||||
|
||||
actors.append(ActorSpec(
|
||||
actor_name=actor_name,
|
||||
asset_key=asset_key,
|
||||
asset_path=asset_path,
|
||||
location=(abs_x, abs_y, abs_z),
|
||||
rotation=rotation,
|
||||
scale=scale,
|
||||
material=material,
|
||||
material_path=mat_path,
|
||||
category=category,
|
||||
zone_id=zone_id,
|
||||
folder=self._get_folder(zone_id, category),
|
||||
note=note,
|
||||
))
|
||||
|
||||
# 累积偏移:沿资产主轴方向排列
|
||||
if asset_info:
|
||||
default_size = asset_info.get("default_size", (0, 0, 0))
|
||||
if default_size and default_size[0] > 0:
|
||||
cum_offset_x += default_size[0] * scale[0]
|
||||
elif size and size[0] > 0 and count > 1:
|
||||
cum_offset_x += size[0] / count
|
||||
|
||||
return actors
|
||||
|
||||
def _make_actor_name(self, zone_id: str, category: str, name: str, seq: int) -> str:
|
||||
"""生成 Actor 名称:LVL_<ZoneID>_<CAT>_<Slug>_<Seq>"""
|
||||
slug = self._slugify(name)
|
||||
return f"LVL_{zone_id}_{category}_{slug}_{seq:02d}"
|
||||
|
||||
def _slugify(self, name: str) -> str:
|
||||
"""将中文名称转为 Actor 名称用的英文缩写。"""
|
||||
# 中文方位→英文
|
||||
_DIR_MAP = {"南": "South", "北": "North", "东": "East", "西": "West",
|
||||
"左": "Left", "右": "Right", "上": "Top", "下": "Bottom"}
|
||||
_TYPE_MAP = {
|
||||
"墙": "Wall", "围": "Wall", "地": "Floor", "顶": "Roof",
|
||||
"门": "Door", "窗": "Window", "栏": "Rail", "柱": "Pillar",
|
||||
"梁": "Beam", "台": "Counter", "架": "Shelf", "柜": "Cabinet",
|
||||
"桌": "Table", "椅": "Chair", "床": "Bed", "箱": "Box",
|
||||
"树": "Tree", "灯": "Lamp", "车": "Car", "桶": "Barrel",
|
||||
}
|
||||
|
||||
result = ""
|
||||
for ch in name:
|
||||
if ch in _DIR_MAP:
|
||||
result += _DIR_MAP[ch]
|
||||
elif ch in _TYPE_MAP:
|
||||
result += _TYPE_MAP[ch]
|
||||
elif ch.isalnum():
|
||||
result += ch
|
||||
# 跳过其他中文字符
|
||||
|
||||
return result if result else "Item"
|
||||
|
||||
def _get_folder(self, zone_id: str, category: str) -> str:
|
||||
"""获取 Outliner 文件夹路径。"""
|
||||
try:
|
||||
from config.zone_config import get_folder_path
|
||||
return get_folder_path(zone_id, category)
|
||||
except ImportError:
|
||||
zone = self.zones.get(zone_id)
|
||||
zone_name = zone.zone_name if zone else "Unknown"
|
||||
return f"LVL_Level/{zone_id}_{zone_name}/{category}"
|
||||
|
||||
def _next_seq(self, zone_id: str, category: str) -> int:
|
||||
"""获取下一个序号。"""
|
||||
key = f"{zone_id}_{category}"
|
||||
if key not in self.actor_counters:
|
||||
self.actor_counters[key] = 0
|
||||
self.actor_counters[key] += 1
|
||||
return self.actor_counters[key]
|
||||
@@ -0,0 +1,260 @@
|
||||
"""组合资产清单 + 模板生成器。
|
||||
|
||||
将执行案中的组合资产标记展开为:
|
||||
1. 组合资产清单 Markdown(供用户审查)
|
||||
2. 组合资产模板 JSON(确认后回馈到 asset-catalog 模板库)
|
||||
|
||||
组合资产 = 无法用单个 BSP StaticMesh 匹配的物件,需要多个 Primitives 拼搭。
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import Optional
|
||||
|
||||
from execution_parser import ExecutionPlanData, CompositeItem
|
||||
|
||||
logger = logging.getLogger("execution_to_guide.composite")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 数据结构
|
||||
# ============================================================
|
||||
|
||||
@dataclass
|
||||
class CompositePart:
|
||||
"""组合资产的一个构件。"""
|
||||
primitive: str # Cube / Cylinder / CirclePlatform
|
||||
scale: tuple # (X, Y, Z)
|
||||
offset: tuple # (X, Y, Z) 相对组合资产原点 cm
|
||||
material: str # MI_DevXxx
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompositeTemplate:
|
||||
"""组合资产模板(确认后回馈到 asset-catalog)。"""
|
||||
template_name: str # 英文标识 OldRadio
|
||||
display_name: str # 中文名 旧收音机
|
||||
source_level: str # 来源版本 v02
|
||||
parts: list # list[CompositePart]
|
||||
total_bounds: dict # {"size": [W, D, H]}
|
||||
tags: list = field(default_factory=list) # ["prop", "electronics"]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 拼搭方案解析
|
||||
# ============================================================
|
||||
|
||||
def parse_build_plan(plan_text: str, target_size: tuple) -> list[CompositePart]:
|
||||
"""解析拼搭方案字符串为构件列表。
|
||||
|
||||
支持的格式:
|
||||
- "Cube(0.2,0.15,0.12) + Cylinder天线"
|
||||
- "Cube主体 + Cylinder(0.02,0.02,0.15)天线 offset(0.05,0,0.12)"
|
||||
|
||||
如果方案不够具体,生成一个默认的 Cube 占位。
|
||||
"""
|
||||
parts = []
|
||||
if not plan_text:
|
||||
# 默认:单个 Cube
|
||||
w, d, h = target_size if target_size != (0, 0, 0) else (20, 20, 20)
|
||||
parts.append(CompositePart(
|
||||
primitive="Cube",
|
||||
scale=(w / 100, d / 100, h / 100),
|
||||
offset=(0, 0, 0),
|
||||
material="MI_DevBrown",
|
||||
))
|
||||
return parts
|
||||
|
||||
# 按 + 分割
|
||||
segments = re.split(r'\s*\+\s*', plan_text)
|
||||
for seg in segments:
|
||||
seg = seg.strip()
|
||||
if not seg:
|
||||
continue
|
||||
|
||||
# 提取 Primitives 类型
|
||||
prim_match = re.match(r'(Cube|Cylinder|CirclePlatform)', seg)
|
||||
prim = prim_match.group(1) if prim_match else "Cube"
|
||||
remaining = seg[prim_match.end():] if prim_match else seg
|
||||
|
||||
# 提取缩放
|
||||
scale_match = re.search(r'\(([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\)', remaining)
|
||||
if scale_match:
|
||||
scale = (float(scale_match.group(1)), float(scale_match.group(2)), float(scale_match.group(3)))
|
||||
else:
|
||||
scale = (0.1, 0.1, 0.1)
|
||||
|
||||
# 提取偏移
|
||||
offset_match = re.search(r'offset\s*\(([-\d.]+)\s*,\s*([-\d.]+)\s*,\s*([-\d.]+)\)', remaining, re.IGNORECASE)
|
||||
offset = (float(offset_match.group(1)), float(offset_match.group(2)), float(offset_match.group(3))) if offset_match else (0, 0, 0)
|
||||
|
||||
# 确定材质
|
||||
material = "MI_DevBrown"
|
||||
if "天线" in seg:
|
||||
material = "MI_DevGray"
|
||||
elif "玻璃" in seg or "镜" in seg:
|
||||
material = "MI_DevGlassWhite"
|
||||
elif "绿" in seg:
|
||||
material = "MI_DevGreen"
|
||||
elif "红" in seg:
|
||||
material = "MI_DevRed"
|
||||
|
||||
parts.append(CompositePart(
|
||||
primitive=prim,
|
||||
scale=scale,
|
||||
offset=offset,
|
||||
material=material,
|
||||
))
|
||||
|
||||
return parts if parts else [CompositePart(
|
||||
primitive="Cube",
|
||||
scale=(0.1, 0.1, 0.1),
|
||||
offset=(0, 0, 0),
|
||||
material="MI_DevBrown",
|
||||
)]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 组合资产文档生成
|
||||
# ============================================================
|
||||
|
||||
class CompositeWriter:
|
||||
"""组合资产清单 + 模板生成器。"""
|
||||
|
||||
def __init__(self, plan_data: ExecutionPlanData):
|
||||
self.plan = plan_data
|
||||
self.templates: list[CompositeTemplate] = []
|
||||
|
||||
def has_composites(self) -> bool:
|
||||
"""是否有组合资产。"""
|
||||
return sum(len(v) for v in self.plan.zone_composites.values()) > 0
|
||||
|
||||
def generate_markdown(self) -> str:
|
||||
"""生成组合资产清单 Markdown。"""
|
||||
lines = [
|
||||
f"# v{self.plan.version} 组合资产清单 — {self.plan.scene_name}",
|
||||
"",
|
||||
"> 本文档列出了执行案中需要用 Primitives 拼搭的组合资产。",
|
||||
"> 请审查每个拼搭方案,确认后可回馈到 asset-catalog 模板库。",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
]
|
||||
|
||||
for zone_id, composites in self.plan.zone_composites.items():
|
||||
if not composites:
|
||||
continue
|
||||
|
||||
zone_name = self._get_zone_name(zone_id)
|
||||
lines.append(f"## {zone_id} {zone_name}")
|
||||
lines.append("")
|
||||
|
||||
for comp in composites:
|
||||
lines.append(f"### {comp.item_id} {comp.name}")
|
||||
lines.append("")
|
||||
lines.append(f"**期望尺寸**: {comp.target_size[0]:.0f}×{comp.target_size[1]:.0f}×{comp.target_size[2]:.0f} cm")
|
||||
lines.append(f"**说明**: {comp.note}")
|
||||
lines.append("")
|
||||
|
||||
# 解析拼搭方案
|
||||
parts = parse_build_plan(comp.build_plan, comp.target_size)
|
||||
|
||||
lines.append("| 构件 | Primitive | 缩放 (X,Y,Z) | 偏移 (X,Y,Z) | 材质 |")
|
||||
lines.append("|------|-----------|-------------|-------------|------|")
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
lines.append(
|
||||
f"| 构件{i + 1} | {part.primitive} | "
|
||||
f"({part.scale[0]:.3f},{part.scale[1]:.3f},{part.scale[2]:.3f}) | "
|
||||
f"({part.offset[0]:.1f},{part.offset[1]:.1f},{part.offset[2]:.1f}) | "
|
||||
f"{part.material} |"
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
|
||||
# 生成模板
|
||||
template = self._make_template(comp, parts)
|
||||
self.templates.append(template)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def generate_templates_json(self) -> str:
|
||||
"""生成组合资产模板 JSON(回馈到 asset-catalog)。"""
|
||||
data = {
|
||||
"version": "1.0",
|
||||
"source_level": self.plan.version,
|
||||
"composite_templates": [asdict(t) for t in self.templates],
|
||||
}
|
||||
# 将 tuple 转为 list 以便 JSON 序列化
|
||||
def convert_tuples(obj):
|
||||
if isinstance(obj, dict):
|
||||
return {k: convert_tuples(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [convert_tuples(i) for i in obj]
|
||||
elif isinstance(obj, tuple):
|
||||
return list(obj)
|
||||
return obj
|
||||
|
||||
data = convert_tuples(data)
|
||||
return json.dumps(data, indent=2, ensure_ascii=False)
|
||||
|
||||
def write_to_files(self, md_path: str, json_path: str = None):
|
||||
"""写入 Markdown 和 JSON 文件。"""
|
||||
# Markdown
|
||||
md_content = self.generate_markdown()
|
||||
md = Path(md_path)
|
||||
md.parent.mkdir(parents=True, exist_ok=True)
|
||||
md.write_text(md_content, encoding="utf-8")
|
||||
logger.info(f"组合资产清单已写入: {md_path}")
|
||||
|
||||
# JSON(可选)
|
||||
if json_path:
|
||||
json_content = self.generate_templates_json()
|
||||
jp = Path(json_path)
|
||||
jp.parent.mkdir(parents=True, exist_ok=True)
|
||||
jp.write_text(json_content, encoding="utf-8")
|
||||
logger.info(f"组合资产模板已写入: {json_path}")
|
||||
|
||||
def _make_template(self, comp: CompositeItem, parts: list[CompositePart]) -> CompositeTemplate:
|
||||
"""从组合资产条目生成模板。"""
|
||||
# 生成英文标识
|
||||
template_name = self._slugify(comp.name)
|
||||
|
||||
# 计算总包围盒
|
||||
max_x = max(p.offset[0] + p.scale[0] * 100 for p in parts) if parts else 0
|
||||
max_y = max(p.offset[1] + p.scale[1] * 100 for p in parts) if parts else 0
|
||||
max_z = max(p.offset[2] + p.scale[2] * 100 for p in parts) if parts else 0
|
||||
|
||||
return CompositeTemplate(
|
||||
template_name=template_name,
|
||||
display_name=comp.name,
|
||||
source_level=self.plan.version,
|
||||
parts=[asdict(p) for p in parts],
|
||||
total_bounds={"size": [max_x, max_y, max_z]},
|
||||
tags=["prop", "composite"],
|
||||
)
|
||||
|
||||
def _get_zone_name(self, zone_id: str) -> str:
|
||||
for z in self.plan.zones:
|
||||
if z.zone_id == zone_id:
|
||||
return z.zone_name
|
||||
return ""
|
||||
|
||||
def _slugify(self, name: str) -> str:
|
||||
"""将中文名称转为英文标识。"""
|
||||
# 简单映射
|
||||
_MAP = {
|
||||
"旧": "Old", "新": "New", "大": "Big", "小": "Small",
|
||||
"收音机": "Radio", "电视": "TV", "冰箱": "Fridge",
|
||||
"空调": "AC", "电话": "Phone", "自行车": "Bicycle",
|
||||
"摩托": "Motorcycle", "柜": "Cabinet", "桌": "Table",
|
||||
"椅": "Chair", "灯": "Lamp", "招牌": "Sign",
|
||||
}
|
||||
result = ""
|
||||
for key, val in sorted(_MAP.items(), key=lambda x: -len(x[0])):
|
||||
if key in name:
|
||||
result += val
|
||||
return result if result else "Composite"
|
||||
@@ -0,0 +1,118 @@
|
||||
"""执行案 → 搭建指南 转换器(主入口)。
|
||||
|
||||
用法:
|
||||
python -m execution_to_guide.converter -i <执行案.md> [-o <搭建指南.md>]
|
||||
|
||||
示例:
|
||||
python converter.py -i docs/design/关卡-v01-城郊小街-执行案.md -o docs/technical/关卡-v01-搭建指南.md
|
||||
python converter.py -i docs/design/关卡-v01-城郊小街-执行案.md # 自动推断输出路径
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from execution_parser import ExecutionPlanParser
|
||||
from guide_writer import GuideWriter
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(name)s: %(message)s",
|
||||
)
|
||||
logger = logging.getLogger("execution_to_guide")
|
||||
|
||||
|
||||
def convert(input_path: str, output_path: str = None) -> str:
|
||||
"""将执行案转换为搭建指南。
|
||||
|
||||
Args:
|
||||
input_path: 执行案 Markdown 文件路径
|
||||
output_path: 搭建指南输出路径(可选,默认自动推断)
|
||||
|
||||
Returns:
|
||||
生成的搭建指南 Markdown 内容
|
||||
"""
|
||||
# 解析执行案
|
||||
logger.info(f"解析执行案: {input_path}")
|
||||
parser = ExecutionPlanParser()
|
||||
plan_data = parser.parse_file(input_path)
|
||||
|
||||
logger.info(f"场景: {plan_data.scene_name} v{plan_data.version}")
|
||||
logger.info(f"尺寸: {plan_data.scene_size[0]}×{plan_data.scene_size[1]}cm")
|
||||
logger.info(f"区域: {len(plan_data.zones)} 个")
|
||||
logger.info(f"结构体: {sum(len(v) for v in plan_data.zone_structures.values())} 条")
|
||||
logger.info(f"布景物: {sum(len(v) for v in plan_data.zone_props.values())} 条")
|
||||
logger.info(f"组合资产: {sum(len(v) for v in plan_data.zone_composites.values())} 条")
|
||||
logger.info(f"全局物件: {len(plan_data.global_objects)} 条")
|
||||
logger.info(f"边界: {len(plan_data.boundaries)} 段")
|
||||
|
||||
# 生成搭建指南
|
||||
writer = GuideWriter(plan_data)
|
||||
content = writer.generate()
|
||||
|
||||
# 输出文件
|
||||
if output_path is None:
|
||||
# 自动推断:docs/design/关卡-v01-xxx-执行案.md → docs/technical/关卡-v01-搭建指南.md
|
||||
in_path = Path(input_path)
|
||||
version = plan_data.version
|
||||
out_dir = in_path.parent.parent / "technical"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = str(out_dir / f"关卡-{version}-搭建指南.md")
|
||||
|
||||
writer.write_to_file(output_path)
|
||||
logger.info(f"搭建指南已输出: {output_path}")
|
||||
|
||||
# 统计
|
||||
total_actors = len(writer.all_actors)
|
||||
unique_assets = len(writer.asset_refs)
|
||||
logger.info(f"生成 Actor: {total_actors} 个,使用资产: {unique_assets} 种")
|
||||
|
||||
# 如果有组合资产,提示
|
||||
total_composites = sum(len(v) for v in plan_data.zone_composites.values())
|
||||
if total_composites > 0:
|
||||
logger.warning(f"⚠️ 发现 {total_composites} 个组合资产需要手动展开或确认拼搭方案")
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="关卡执行案 → 搭建指南 转换器",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
示例:
|
||||
python -m execution_to_guide.converter -i docs/design/关卡-v01-城郊小街-执行案.md
|
||||
python converter.py -i docs/design/关卡-v01-城郊小街-执行案.md -o output/指南.md
|
||||
""",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i", "--input", required=True,
|
||||
help="关卡执行案 Markdown 文件路径"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", "--output", default=None,
|
||||
help="搭建指南输出路径(默认自动推断到 docs/technical/)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="store_true",
|
||||
help="显示详细日志"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
try:
|
||||
convert(args.input, args.output)
|
||||
except FileNotFoundError as e:
|
||||
logger.error(f"文件未找到: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"转换失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,662 @@
|
||||
"""关卡执行案 Markdown 解析器。
|
||||
|
||||
解析 docs/design/关卡-vXX-<名称>-执行案.md,提取结构化数据供后续转换为搭建指南。
|
||||
|
||||
执行案的表格格式(简化版,无交互物/收集品):
|
||||
- 区域总览表: 区域编号 / 区域名称 / 绝对坐标范围 (X,Y) / 尺寸 / 地面材质建议 / 标识色
|
||||
- 交通流线表: 流线路径 / 宽度 / 说明
|
||||
- 结构体表: 编号 / 构件名称 / 尺寸 / 相对原点偏移 / BSP 资产建议 / 标识色
|
||||
- 布景表: 编号 / 物件名称 / 尺寸 / 相对原点偏移 / BSP 资产建议 / 说明
|
||||
- 组合资产表: 编号 / 物件名称 / 期望尺寸 / 推荐拼搭方案 / 说明
|
||||
- 全局物件表: 编号 / 物件名称 / 数量 / 放置位置 / BSP 资产建议 / 说明
|
||||
- 边界表: 边界段 / 位置范围 / 遮挡方式 / BSP 资产建议 / 标识色
|
||||
- 光照表: 光源类型 / 数量 / 位置/方向 / 参数建议 / 说明
|
||||
"""
|
||||
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
# 允许从包内或直接运行
|
||||
try:
|
||||
from parsers.markdown_table import parse_table, clean_cell
|
||||
except ImportError:
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "level-builder"))
|
||||
from parsers.markdown_table import parse_table, clean_cell
|
||||
|
||||
logger = logging.getLogger("execution_to_guide.parser")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 数据结构
|
||||
# ============================================================
|
||||
|
||||
@dataclass
|
||||
class ZoneInfo:
|
||||
"""区域信息。"""
|
||||
zone_id: str # Z1, Z2, ...
|
||||
zone_name: str # 家、小卖部、...
|
||||
origin: tuple # (X, Y, Z) 区域西南角绝对坐标
|
||||
bounds_x: tuple # (X_min, X_max)
|
||||
bounds_y: tuple # (Y_min, Y_max)
|
||||
size_m: tuple # (宽m, 深m)
|
||||
ground_material: str = "" # 地面材质建议
|
||||
tag_color: str = "" # 标识色 MI_DevXxx
|
||||
height: float = 0 # 建筑高度(cm)
|
||||
wall_height: float = 250 # 围墙高度
|
||||
door_width: float = 120 # 门洞宽度
|
||||
door_height: float = 220 # 门洞高度
|
||||
indoor_clear_height: float = 280 # 室内净高
|
||||
eaves_height: float = 300 # 屋檐高度
|
||||
|
||||
|
||||
@dataclass
|
||||
class StructureItem:
|
||||
"""结构体条目(墙/地/顶/门/窗等建筑构件)。"""
|
||||
item_id: str # Z1-S01
|
||||
name: str # 南围墙
|
||||
size: tuple # (宽, 深, 高) cm
|
||||
offset: tuple # (ΔX, ΔY, ΔZ) 相对区域原点
|
||||
bsp_suggestion: str # Wall_400x300 x 2
|
||||
tag_color: str = "" # MI_DevRed
|
||||
|
||||
|
||||
@dataclass
|
||||
class PropItem:
|
||||
"""布景物条目(家具/道具/装饰)。"""
|
||||
item_id: str # Z1-P01
|
||||
name: str # 旧自行车
|
||||
size: tuple # (宽, 深, 高) cm(约)
|
||||
offset: tuple # (ΔX, ΔY, ΔZ) 相对区域原点
|
||||
bsp_suggestion: str # Car01 缩放50%
|
||||
note: str = "" # 靠墙停放
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompositeItem:
|
||||
"""组合资产条目(需 Primitives 拼搭)。"""
|
||||
item_id: str # Z1-C01
|
||||
name: str # 旧收音机
|
||||
target_size: tuple # (宽, 深, 高) cm(约)
|
||||
build_plan: str # Cube(0.2,0.15,0.12) + Cylinder天线
|
||||
note: str = "" # 放在柜台上
|
||||
|
||||
|
||||
@dataclass
|
||||
class GlobalObject:
|
||||
"""全局物件。"""
|
||||
item_id: str # G-01 / G-E01
|
||||
name: str
|
||||
count: int
|
||||
location: tuple # (X, Y, Z) 绝对坐标
|
||||
bsp_suggestion: str
|
||||
note: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class BoundaryItem:
|
||||
"""场景边界。"""
|
||||
segment: str # 西边界/南边界/东边界/北边界
|
||||
location_range: str
|
||||
block_method: str # 围墙/自然遮挡
|
||||
bsp_suggestion: str
|
||||
tag_color: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class LightItem:
|
||||
"""光照建议。"""
|
||||
light_type: str # 定向光/天空光/路灯
|
||||
count: int
|
||||
position: str # 位置/方向描述
|
||||
params: str # 参数建议
|
||||
note: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrafficFlow:
|
||||
"""交通流线。"""
|
||||
path: str # Z1 → Z3 家门 → 街道
|
||||
width: float # cm
|
||||
note: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionPlanData:
|
||||
"""执行案解析后的完整数据。"""
|
||||
version: str = ""
|
||||
scene_name: str = ""
|
||||
scene_size: tuple = (0, 0) # (宽cm, 深cm)
|
||||
ground_z: float = 0
|
||||
character_height: float = 120
|
||||
camera_height: float = 100
|
||||
time_setting: str = ""
|
||||
zones: list = field(default_factory=list) # list[ZoneInfo]
|
||||
flows: list = field(default_factory=list) # list[TrafficFlow]
|
||||
zone_structures: dict = field(default_factory=dict) # zone_id -> list[StructureItem]
|
||||
zone_props: dict = field(default_factory=dict) # zone_id -> list[PropItem]
|
||||
zone_composites: dict = field(default_factory=dict) # zone_id -> list[CompositeItem]
|
||||
global_objects: list = field(default_factory=list) # list[GlobalObject]
|
||||
boundaries: list = field(default_factory=list) # list[BoundaryItem]
|
||||
lights: list = field(default_factory=list) # list[LightItem]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 解析逻辑
|
||||
# ============================================================
|
||||
|
||||
class ExecutionPlanParser:
|
||||
"""关卡执行案解析器。"""
|
||||
|
||||
def __init__(self):
|
||||
self.data = ExecutionPlanData()
|
||||
|
||||
def parse_file(self, filepath: str) -> ExecutionPlanData:
|
||||
"""解析执行案文件。"""
|
||||
path = Path(filepath)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"执行案文件不存在: {filepath}")
|
||||
text = path.read_text(encoding="utf-8")
|
||||
return self.parse_text(text)
|
||||
|
||||
def parse_text(self, text: str) -> ExecutionPlanData:
|
||||
"""解析执行案文本。"""
|
||||
lines = text.split("\n")
|
||||
|
||||
# 解析标题 → 版本和场景名
|
||||
self._parse_title(lines)
|
||||
|
||||
# 一、场景参数
|
||||
self._parse_scene_params(lines)
|
||||
|
||||
# 二、区域坐标布局
|
||||
self._parse_zone_layout(lines)
|
||||
|
||||
# 三、逐区域搭建指南
|
||||
self._parse_zone_details(lines)
|
||||
|
||||
# 四、全局物件清单
|
||||
self._parse_global_objects(lines)
|
||||
|
||||
# 五、场景边界与遮挡
|
||||
self._parse_boundaries(lines)
|
||||
|
||||
# 六、光照建议
|
||||
self._parse_lighting(lines)
|
||||
|
||||
return self.data
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 标题
|
||||
# ----------------------------------------------------------
|
||||
def _parse_title(self, lines: list[str]):
|
||||
"""从标题提取版本号和场景名。如:# v0.1 关卡执行案 — 城郊小街"""
|
||||
for line in lines:
|
||||
m = re.match(r'#\s+v([\d.]+)\s+关卡执行案\s*[—\-–]\s*(.+)', line)
|
||||
if m:
|
||||
self.data.version = m.group(1)
|
||||
self.data.scene_name = m.group(2).strip()
|
||||
break
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 一、场景参数
|
||||
# ----------------------------------------------------------
|
||||
def _parse_scene_params(self, lines: list[str]):
|
||||
"""解析场景参数表。"""
|
||||
section_start = self._find_section(lines, "一、场景参数")
|
||||
section_end = self._find_section(lines, "二、区域坐标布局")
|
||||
if section_start < 0:
|
||||
return
|
||||
if section_end < 0:
|
||||
section_end = len(lines)
|
||||
|
||||
section_lines = lines[section_start:section_end]
|
||||
tables = self._extract_tables(section_lines)
|
||||
if not tables:
|
||||
return
|
||||
|
||||
for row in tables[0]:
|
||||
param = clean_cell(row.get("参数", ""))
|
||||
value = clean_cell(row.get("值", ""))
|
||||
if not param:
|
||||
continue
|
||||
|
||||
if "场景总尺寸" in param:
|
||||
size_match = re.search(r'(\d+)\s*cm\s*[x×]\s*(\d+)\s*cm', value)
|
||||
if size_match:
|
||||
self.data.scene_size = (int(size_match.group(1)), int(size_match.group(2)))
|
||||
elif "地面高度基准" in param:
|
||||
z_match = re.search(r'Z?\s*=\s*(-?\d+)', value)
|
||||
if z_match:
|
||||
self.data.ground_z = float(z_match.group(1))
|
||||
elif "主角身高" in param:
|
||||
h_match = re.search(r'~?(\d+)', value)
|
||||
if h_match:
|
||||
self.data.character_height = float(h_match.group(1))
|
||||
elif "视角高度" in param:
|
||||
h_match = re.search(r'~?(\d+)', value)
|
||||
if h_match:
|
||||
self.data.camera_height = float(h_match.group(1))
|
||||
elif "时间设定" in param:
|
||||
self.data.time_setting = value
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 二、区域坐标布局
|
||||
# ----------------------------------------------------------
|
||||
def _parse_zone_layout(self, lines: list[str]):
|
||||
"""解析区域总览表和交通流线表。"""
|
||||
section_start = self._find_section(lines, "二、区域坐标布局")
|
||||
section_end = self._find_section(lines, "三、逐区域搭建指南")
|
||||
if section_start < 0:
|
||||
return
|
||||
if section_end < 0:
|
||||
section_end = len(lines)
|
||||
|
||||
section_lines = lines[section_start:section_end]
|
||||
tables = self._extract_tables(section_lines)
|
||||
|
||||
# 第一个表格 = 区域总览表
|
||||
if tables:
|
||||
for row in tables[0]:
|
||||
zone = self._parse_zone_row(row)
|
||||
if zone:
|
||||
self.data.zones.append(zone)
|
||||
|
||||
# 第二个表格 = 交通流线(如果有)
|
||||
if len(tables) > 1:
|
||||
for row in tables[1]:
|
||||
path = clean_cell(row.get("流线路径", ""))
|
||||
if not path:
|
||||
continue
|
||||
width_str = clean_cell(row.get("宽度", "200"))
|
||||
w_match = re.search(r'(\d+)', width_str)
|
||||
width = float(w_match.group(1)) if w_match else 200.0
|
||||
note = clean_cell(row.get("说明", ""))
|
||||
self.data.flows.append(TrafficFlow(
|
||||
path=path, width=width, note=note
|
||||
))
|
||||
|
||||
def _parse_zone_row(self, row: dict) -> Optional[ZoneInfo]:
|
||||
"""解析区域总览表的一行。"""
|
||||
zone_id = clean_cell(row.get("区域编号", ""))
|
||||
zone_name = clean_cell(row.get("区域名称", ""))
|
||||
coord_str = clean_cell(row.get("绝对坐标范围 (X,Y)", row.get("绝对坐标范围", "")))
|
||||
|
||||
if not zone_id or not zone_name:
|
||||
return None
|
||||
if not re.match(r'Z\d+', zone_id):
|
||||
return None
|
||||
|
||||
# 解析坐标范围:(X1,Y1)→(X2,Y2)
|
||||
coord_match = re.match(
|
||||
r'\((\d+)\s*,\s*(\d+)\)\s*[→\-–]+\s*\((\d+)\s*,\s*(\d+)\)',
|
||||
coord_str
|
||||
)
|
||||
if not coord_match:
|
||||
return None
|
||||
|
||||
x_min, y_min = int(coord_match.group(1)), int(coord_match.group(2))
|
||||
x_max, y_max = int(coord_match.group(3)), int(coord_match.group(4))
|
||||
|
||||
# 解析尺寸
|
||||
size_str = clean_cell(row.get("尺寸 (宽x深)", row.get("尺寸", "")))
|
||||
size_match = re.search(r'(\d+)\s*m?\s*[x×]\s*(\d+)\s*m?', size_str)
|
||||
size_m = (float(size_match.group(1)), float(size_match.group(2))) if size_match else (0, 0)
|
||||
|
||||
ground_mat = clean_cell(row.get("地面材质建议", ""))
|
||||
tag_color = clean_cell(row.get("标识色", ""))
|
||||
|
||||
return ZoneInfo(
|
||||
zone_id=zone_id,
|
||||
zone_name=zone_name,
|
||||
origin=(x_min, y_min, 0),
|
||||
bounds_x=(x_min, x_max),
|
||||
bounds_y=(y_min, y_max),
|
||||
size_m=size_m,
|
||||
ground_material=ground_mat,
|
||||
tag_color=tag_color,
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 三、逐区域搭建指南
|
||||
# ----------------------------------------------------------
|
||||
def _parse_zone_details(self, lines: list[str]):
|
||||
"""解析逐区域搭建指南(结构体/布景/组合资产)。"""
|
||||
section_start = self._find_section(lines, "三、逐区域搭建指南")
|
||||
section_end = self._find_section(lines, "四、全局物件清单")
|
||||
if section_start < 0:
|
||||
return
|
||||
if section_end < 0:
|
||||
section_end = len(lines)
|
||||
|
||||
section_lines = lines[section_start:section_end]
|
||||
current_zone = ""
|
||||
current_zone_info = None
|
||||
|
||||
i = 0
|
||||
while i < len(section_lines):
|
||||
line = section_lines[i].strip()
|
||||
|
||||
# 检测区域标题:### Z1:家(Home)或 ### Z1: 家 或 ### Z1:家
|
||||
zm = re.match(r'###\s+(Z\d+)\s*[::]\s*(.+?)(?:\s*[((].+[)])?\s*$', line)
|
||||
if zm:
|
||||
current_zone = zm.group(1)
|
||||
current_zone_info = self._find_zone(current_zone)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 检测区域绝对原点行:**区域绝对原点**:(X, Y, Z)
|
||||
if "区域绝对原点" in line and current_zone:
|
||||
origin_match = re.search(r'\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)', line)
|
||||
if origin_match and current_zone_info:
|
||||
current_zone_info.origin = (
|
||||
int(origin_match.group(1)),
|
||||
int(origin_match.group(2)),
|
||||
int(origin_match.group(3)),
|
||||
)
|
||||
|
||||
# 检测区域参数子标题
|
||||
if "区域参数" in line:
|
||||
i += 1
|
||||
i = self._skip_blank(section_lines, i)
|
||||
rows = self._read_table(section_lines, i)
|
||||
if rows:
|
||||
i = self._advance_past_table(section_lines, i)
|
||||
if current_zone_info:
|
||||
self._parse_zone_params(rows, current_zone_info)
|
||||
continue
|
||||
|
||||
# 检测结构体子标题
|
||||
if "结构体清单" in line:
|
||||
i += 1
|
||||
i = self._skip_blank(section_lines, i)
|
||||
rows = self._read_table(section_lines, i)
|
||||
if rows and current_zone:
|
||||
items = [self._parse_structure_row(r) for r in rows]
|
||||
items = [it for it in items if it]
|
||||
self.data.zone_structures[current_zone] = items
|
||||
i = self._advance_past_table(section_lines, i)
|
||||
continue
|
||||
|
||||
# 检测布景物子标题
|
||||
if "布景物清单" in line:
|
||||
i += 1
|
||||
i = self._skip_blank(section_lines, i)
|
||||
rows = self._read_table(section_lines, i)
|
||||
if rows and current_zone:
|
||||
items = [self._parse_prop_row(r) for r in rows]
|
||||
items = [it for it in items if it]
|
||||
self.data.zone_props[current_zone] = items
|
||||
i = self._advance_past_table(section_lines, i)
|
||||
continue
|
||||
|
||||
# 检测组合资产子标题
|
||||
if "组合资产标记" in line:
|
||||
i += 1
|
||||
i = self._skip_blank(section_lines, i)
|
||||
rows = self._read_table(section_lines, i)
|
||||
if rows and current_zone:
|
||||
items = [self._parse_composite_row(r) for r in rows]
|
||||
items = [it for it in items if it]
|
||||
self.data.zone_composites[current_zone] = items
|
||||
i = self._advance_past_table(section_lines, i)
|
||||
continue
|
||||
|
||||
i += 1
|
||||
|
||||
def _parse_zone_params(self, rows: list[dict], zone: ZoneInfo):
|
||||
"""解析区域参数表。"""
|
||||
for row in rows:
|
||||
param = clean_cell(row.get("参数", ""))
|
||||
value = clean_cell(row.get("值", ""))
|
||||
if "围墙" in param or "外墙高度" in param:
|
||||
m = re.search(r'(\d+)', value)
|
||||
if m:
|
||||
zone.wall_height = float(m.group(1))
|
||||
elif "门洞宽度" in param:
|
||||
m = re.search(r'(\d+)', value)
|
||||
if m:
|
||||
zone.door_width = float(m.group(1))
|
||||
elif "门洞高度" in param:
|
||||
m = re.search(r'(\d+)', value)
|
||||
if m:
|
||||
zone.door_height = float(m.group(1))
|
||||
elif "室内净高" in param:
|
||||
m = re.search(r'(\d+)', value)
|
||||
if m:
|
||||
zone.indoor_clear_height = float(m.group(1))
|
||||
elif "屋檐高度" in param:
|
||||
m = re.search(r'(\d+)', value)
|
||||
if m:
|
||||
zone.eaves_height = float(m.group(1))
|
||||
|
||||
def _parse_structure_row(self, row: dict) -> Optional[StructureItem]:
|
||||
"""解析结构体表行。"""
|
||||
item_id = clean_cell(row.get("编号", ""))
|
||||
name = clean_cell(row.get("构件名称", ""))
|
||||
if not item_id or not name:
|
||||
return None
|
||||
|
||||
size_str = clean_cell(row.get("尺寸 (宽x深x高, cm)", row.get("尺寸", "")))
|
||||
size = self._parse_size(size_str)
|
||||
|
||||
offset_str = clean_cell(row.get("相对原点偏移 (ΔX,ΔY,ΔZ)", row.get("相对原点偏移", "")))
|
||||
offset = self._parse_xyz(offset_str)
|
||||
|
||||
bsp = clean_cell(row.get("BSP 资产建议", ""))
|
||||
tag = clean_cell(row.get("标识色", ""))
|
||||
|
||||
return StructureItem(
|
||||
item_id=item_id, name=name, size=size,
|
||||
offset=offset, bsp_suggestion=bsp, tag_color=tag
|
||||
)
|
||||
|
||||
def _parse_prop_row(self, row: dict) -> Optional[PropItem]:
|
||||
"""解析布景表行。"""
|
||||
item_id = clean_cell(row.get("编号", ""))
|
||||
name = clean_cell(row.get("物件名称", ""))
|
||||
if not item_id or not name:
|
||||
return None
|
||||
|
||||
size_str = clean_cell(row.get("尺寸 (约, cm)", row.get("尺寸", "")))
|
||||
size = self._parse_size(size_str)
|
||||
|
||||
offset_str = clean_cell(row.get("相对原点偏移 (ΔX,ΔY,ΔZ)", row.get("相对原点偏移", "")))
|
||||
offset = self._parse_xyz(offset_str)
|
||||
|
||||
bsp = clean_cell(row.get("BSP 资产建议", ""))
|
||||
note = clean_cell(row.get("说明", ""))
|
||||
|
||||
return PropItem(
|
||||
item_id=item_id, name=name, size=size,
|
||||
offset=offset, bsp_suggestion=bsp, note=note
|
||||
)
|
||||
|
||||
def _parse_composite_row(self, row: dict) -> Optional[CompositeItem]:
|
||||
"""解析组合资产表行。"""
|
||||
item_id = clean_cell(row.get("编号", ""))
|
||||
name = clean_cell(row.get("物件名称", ""))
|
||||
if not item_id or not name:
|
||||
return None
|
||||
|
||||
size_str = clean_cell(row.get("期望尺寸 (约, cm)", row.get("期望尺寸", "")))
|
||||
size = self._parse_size(size_str)
|
||||
|
||||
plan = clean_cell(row.get("推荐拼搭方案", ""))
|
||||
note = clean_cell(row.get("说明", ""))
|
||||
|
||||
return CompositeItem(
|
||||
item_id=item_id, name=name, target_size=size,
|
||||
build_plan=plan, note=note
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 四、全局物件清单
|
||||
# ----------------------------------------------------------
|
||||
def _parse_global_objects(self, lines: list[str]):
|
||||
"""解析全局物件清单。"""
|
||||
section_start = self._find_section(lines, "四、全局物件清单")
|
||||
section_end = self._find_section(lines, "五、场景边界")
|
||||
if section_start < 0:
|
||||
return
|
||||
if section_end < 0:
|
||||
section_end = len(lines)
|
||||
|
||||
section_lines = lines[section_start:section_end]
|
||||
tables = self._extract_tables(section_lines)
|
||||
|
||||
for table in tables:
|
||||
for row in table:
|
||||
item_id = clean_cell(row.get("编号", ""))
|
||||
name = clean_cell(row.get("物件名称", ""))
|
||||
if not item_id or not name:
|
||||
continue
|
||||
|
||||
count_str = clean_cell(row.get("数量", "1"))
|
||||
c_match = re.search(r'(\d+)', count_str)
|
||||
count = int(c_match.group(1)) if c_match else 1
|
||||
|
||||
loc_str = clean_cell(row.get("放置位置 (绝对坐标 X,Y,Z)", row.get("放置位置", "")))
|
||||
location = self._parse_xyz(loc_str)
|
||||
|
||||
bsp = clean_cell(row.get("BSP 资产建议", ""))
|
||||
note = clean_cell(row.get("说明", ""))
|
||||
|
||||
self.data.global_objects.append(GlobalObject(
|
||||
item_id=item_id, name=name, count=count,
|
||||
location=location, bsp_suggestion=bsp, note=note
|
||||
))
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 五、场景边界与遮挡
|
||||
# ----------------------------------------------------------
|
||||
def _parse_boundaries(self, lines: list[str]):
|
||||
"""解析场景边界表。"""
|
||||
section_start = self._find_section(lines, "五、场景边界")
|
||||
section_end = self._find_section(lines, "六、光照建议")
|
||||
if section_start < 0:
|
||||
return
|
||||
if section_end < 0:
|
||||
section_end = len(lines)
|
||||
|
||||
section_lines = lines[section_start:section_end]
|
||||
tables = self._extract_tables(section_lines)
|
||||
|
||||
if tables:
|
||||
for row in tables[0]:
|
||||
segment = clean_cell(row.get("边界段", ""))
|
||||
if not segment:
|
||||
continue
|
||||
loc_range = clean_cell(row.get("位置范围", ""))
|
||||
method = clean_cell(row.get("遮挡方式", ""))
|
||||
bsp = clean_cell(row.get("BSP 资产建议", ""))
|
||||
tag = clean_cell(row.get("标识色", ""))
|
||||
self.data.boundaries.append(BoundaryItem(
|
||||
segment=segment, location_range=loc_range,
|
||||
block_method=method, bsp_suggestion=bsp, tag_color=tag
|
||||
))
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 六、光照建议
|
||||
# ----------------------------------------------------------
|
||||
def _parse_lighting(self, lines: list[str]):
|
||||
"""解析光照建议表。"""
|
||||
section_start = self._find_section(lines, "六、光照建议")
|
||||
if section_start < 0:
|
||||
return
|
||||
|
||||
section_lines = lines[section_start:]
|
||||
tables = self._extract_tables(section_lines)
|
||||
|
||||
if tables:
|
||||
for row in tables[0]:
|
||||
light_type = clean_cell(row.get("光源类型", ""))
|
||||
if not light_type:
|
||||
continue
|
||||
count_str = clean_cell(row.get("数量", "1"))
|
||||
c_match = re.search(r'(\d+)', count_str)
|
||||
count = int(c_match.group(1)) if c_match else 1
|
||||
position = clean_cell(row.get("位置/方向", ""))
|
||||
params = clean_cell(row.get("参数建议", ""))
|
||||
note = clean_cell(row.get("说明", ""))
|
||||
self.data.lights.append(LightItem(
|
||||
light_type=light_type, count=count,
|
||||
position=position, params=params, note=note
|
||||
))
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 辅助方法
|
||||
# ----------------------------------------------------------
|
||||
def _find_section(self, lines: list[str], keyword: str) -> int:
|
||||
"""找到包含关键词的章节标题行。"""
|
||||
for i, line in enumerate(lines):
|
||||
if keyword in line:
|
||||
return i
|
||||
return -1
|
||||
|
||||
def _find_zone(self, zone_id: str) -> Optional[ZoneInfo]:
|
||||
"""查找区域信息。"""
|
||||
for z in self.data.zones:
|
||||
if z.zone_id == zone_id:
|
||||
return z
|
||||
return None
|
||||
|
||||
def _extract_tables(self, lines: list[str]) -> list[list[dict]]:
|
||||
"""从行列表中提取所有表格。"""
|
||||
tables = []
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
if '|' in lines[i]:
|
||||
table_lines = []
|
||||
while i < len(lines) and '|' in lines[i]:
|
||||
table_lines.append(lines[i])
|
||||
i += 1
|
||||
rows = parse_table(table_lines)
|
||||
if rows:
|
||||
tables.append(rows)
|
||||
continue
|
||||
i += 1
|
||||
return tables
|
||||
|
||||
def _skip_blank(self, lines: list[str], i: int) -> int:
|
||||
"""跳过空行,返回第一个非空行的索引。"""
|
||||
while i < len(lines) and not lines[i].strip():
|
||||
i += 1
|
||||
return i
|
||||
|
||||
def _read_table(self, lines: list[str], i: int) -> list[dict]:
|
||||
"""从第 i 行开始读取一个 Markdown 表格。"""
|
||||
table_lines = []
|
||||
while i < len(lines) and '|' in lines[i]:
|
||||
table_lines.append(lines[i])
|
||||
i += 1
|
||||
if not table_lines:
|
||||
return []
|
||||
return parse_table(table_lines)
|
||||
|
||||
def _advance_past_table(self, lines: list[str], i: int) -> int:
|
||||
"""将索引推进到表格之后。"""
|
||||
while i < len(lines) and '|' in lines[i]:
|
||||
i += 1
|
||||
return i
|
||||
|
||||
def _parse_xyz(self, text: str) -> tuple:
|
||||
"""解析 (X,Y,Z) 或 (ΔX,ΔY,ΔZ) 格式。"""
|
||||
if not text:
|
||||
return (0, 0, 0)
|
||||
m = re.search(r'\(([-\d.]+)\s*[,,]\s*([-\d.]+)\s*[,,]\s*([-\d.]+)\)', text)
|
||||
if m:
|
||||
return (float(m.group(1)), float(m.group(2)), float(m.group(3)))
|
||||
return (0, 0, 0)
|
||||
|
||||
def _parse_size(self, text: str) -> tuple:
|
||||
"""解析 尺寸格式:宽x深x高。支持 '800x20x250' 或 '800×20×250'。"""
|
||||
if not text:
|
||||
return (0, 0, 0)
|
||||
m = re.search(r'(\d+(?:\.\d+)?)\s*[x×]\s*(\d+(?:\.\d+)?)\s*[x×]\s*(\d+(?:\.\d+)?)', text)
|
||||
if m:
|
||||
return (float(m.group(1)), float(m.group(2)), float(m.group(3)))
|
||||
return (0, 0, 0)
|
||||
@@ -0,0 +1,596 @@
|
||||
"""搭建指南 Markdown 生成器。
|
||||
|
||||
将 ActorSpec 列表转换为搭建指南 Markdown 文档。
|
||||
输出格式精确匹配 build_guide_parser.py 的解析预期。
|
||||
|
||||
搭建指南结构(8章):
|
||||
〇、全局信息(场景参数、路径前缀、命名规范、Outliner 结构)
|
||||
一、BSP 资产映射表
|
||||
二、Phase B 地面与边界
|
||||
三、Phase C 逐区域搭建
|
||||
四、Phase C 全局物件
|
||||
五、Phase D 光照
|
||||
六、Phase E 验证清单
|
||||
"""
|
||||
|
||||
import logging
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from execution_parser import (
|
||||
ExecutionPlanData, ZoneInfo, StructureItem, PropItem,
|
||||
CompositeItem, GlobalObject, BoundaryItem, LightItem,
|
||||
)
|
||||
from asset_expander import AssetExpander, ActorSpec
|
||||
|
||||
logger = logging.getLogger("execution_to_guide.writer")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 搭建指南生成器
|
||||
# ============================================================
|
||||
|
||||
class GuideWriter:
|
||||
"""搭建指南 Markdown 生成器。"""
|
||||
|
||||
def __init__(self, plan_data: ExecutionPlanData):
|
||||
"""
|
||||
Args:
|
||||
plan_data: 解析后的执行案数据
|
||||
"""
|
||||
self.plan = plan_data
|
||||
self.expander = AssetExpander(plan_data.zones)
|
||||
self.all_actors: list[ActorSpec] = []
|
||||
self.asset_refs: dict[str, dict] = {} # 用到的资产 key → info
|
||||
|
||||
def generate(self) -> str:
|
||||
"""生成完整的搭建指南 Markdown。"""
|
||||
sections = []
|
||||
|
||||
# 〇、全局信息
|
||||
sections.append(self._write_global_info())
|
||||
|
||||
# 一、BSP 资产映射表
|
||||
sections.append(self._write_asset_mapping())
|
||||
|
||||
# 二、Phase B — 地面与边界
|
||||
sections.append(self._write_ground_and_boundary())
|
||||
|
||||
# 三、Phase C — 逐区域搭建
|
||||
sections.append(self._write_zone_builds())
|
||||
|
||||
# 四、Phase C — 全局物件
|
||||
sections.append(self._write_global_objects())
|
||||
|
||||
# 五、Phase D — 光照
|
||||
sections.append(self._write_lighting())
|
||||
|
||||
# 六、Phase E — 验证清单
|
||||
sections.append(self._write_checklist())
|
||||
|
||||
return "\n\n".join(sections)
|
||||
|
||||
def write_to_file(self, filepath: str):
|
||||
"""生成并写入文件。"""
|
||||
content = self.generate()
|
||||
path = Path(filepath)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
logger.info(f"搭建指南已写入: {filepath}")
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 〇、全局信息
|
||||
# ----------------------------------------------------------
|
||||
def _write_global_info(self) -> str:
|
||||
"""生成全局信息章节。"""
|
||||
w, d = self.plan.scene_size
|
||||
lines = [
|
||||
f"# v{self.plan.version} 白模搭建操作指南 — {self.plan.scene_name}",
|
||||
"",
|
||||
f"> 本文档由 execution-to-guide 工具自动生成,供 level-builder 搭建白模关卡使用。",
|
||||
f"> 基于: 关卡-v{self.plan.version}-{self.plan.scene_name}-执行案.md",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"## 〇、全局信息",
|
||||
"",
|
||||
"### 场景参数",
|
||||
"",
|
||||
"| 参数 | 值 |",
|
||||
"|------|-----|",
|
||||
f"| 场景总尺寸 | {w}cm × {d}cm({w // 100}m × {d // 100}m) |",
|
||||
f"| 地面高度基准 | Z = {int(self.plan.ground_z)} |",
|
||||
f"| 主角身高 | ~{int(self.plan.character_height)}cm |",
|
||||
f"| 摄像机高度 | ~{int(self.plan.camera_height)}cm |",
|
||||
f"| 时间设定 | {self.plan.time_setting} |",
|
||||
"| 坐标原点 | 场景西南角地平面 |",
|
||||
"",
|
||||
"### UE5 路径前缀",
|
||||
"",
|
||||
"- **Mesh**: `Content/BlockingStarterPack/Meshes/`",
|
||||
"- **Material**: `Content/BlockingStarterPack/Materials/`",
|
||||
"- **Material (Basics)**: `Content/BlockingStarterPack/Materials/Blocking/Basics/`",
|
||||
"",
|
||||
"### 命名规范",
|
||||
"",
|
||||
"```",
|
||||
"LVL_<ZoneID>_<Category>_<Description>",
|
||||
"Category: STR | PROP | COMP | BND | LGT",
|
||||
"```",
|
||||
"",
|
||||
"### Outliner 文件夹结构",
|
||||
"",
|
||||
"```",
|
||||
f"LVL_{self._level_slug()}/",
|
||||
]
|
||||
|
||||
for zone in self.plan.zones:
|
||||
zone_slug = f"{zone.zone_id}_{zone.zone_name}"
|
||||
lines.append(f" {zone_slug}/")
|
||||
lines.append(" Structure/ Props/")
|
||||
|
||||
lines.append(" Global/")
|
||||
lines.append(" Boundaries/ Lighting/ PlayerStart/")
|
||||
lines.append("```")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 一、BSP 资产映射表
|
||||
# ----------------------------------------------------------
|
||||
def _write_asset_mapping(self) -> str:
|
||||
"""收集用到的所有资产,生成映射表。"""
|
||||
# 先展开所有 Actor 以收集资产引用
|
||||
self._collect_all_actors()
|
||||
|
||||
lines = [
|
||||
"---",
|
||||
"",
|
||||
"## 一、BSP 资产映射表",
|
||||
"",
|
||||
]
|
||||
|
||||
# 按类别分组
|
||||
categories = {}
|
||||
for key, info in self.asset_refs.items():
|
||||
cat = info.get("category", "other")
|
||||
if cat not in categories:
|
||||
categories[cat] = []
|
||||
categories[cat].append((key, info))
|
||||
|
||||
# 类别标题映射
|
||||
cat_titles = {
|
||||
"wall": "建筑 — 墙体",
|
||||
"wall_door": "建筑 — 门墙",
|
||||
"wall_window": "建筑 — 窗墙",
|
||||
"floor": "建筑 — 地板",
|
||||
"door": "建筑 — 门",
|
||||
"window": "建筑 — 窗户",
|
||||
"barrier": "建筑 — 围挡",
|
||||
"fence": "建筑 — 围栏",
|
||||
"beam": "建筑 — 横梁",
|
||||
"building": "建筑 — 大型建筑",
|
||||
"furniture": "家具",
|
||||
"industrial": "工业/装饰",
|
||||
"vehicle": "载具",
|
||||
"primitive": "基础体",
|
||||
"vegetation": "植被",
|
||||
"light_fixture": "灯具",
|
||||
"decoration": "装饰",
|
||||
"other": "其他",
|
||||
}
|
||||
|
||||
for cat_key in ["wall", "wall_door", "wall_window", "floor", "door", "window",
|
||||
"barrier", "fence", "beam", "building", "furniture",
|
||||
"industrial", "vehicle", "vegetation", "light_fixture",
|
||||
"decoration", "other"]:
|
||||
if cat_key not in categories:
|
||||
continue
|
||||
items = categories[cat_key]
|
||||
title = cat_titles.get(cat_key, cat_key)
|
||||
|
||||
lines.append(f"### {title}")
|
||||
lines.append("")
|
||||
lines.append("| 设计文档引用 | UE5 资产路径 | 实际尺寸 (cm) |")
|
||||
lines.append("|-------------|-------------|--------------|")
|
||||
|
||||
for key, info in items:
|
||||
path_short = info.get("path", "")
|
||||
# 缩短路径显示
|
||||
if path_short.startswith("/Game/BlockingStarterPack/Meshes/"):
|
||||
path_short = path_short.replace("/Game/BlockingStarterPack/Meshes/", "")
|
||||
path_short = path_short.split(".")[0] # 去掉 .SM_xxx 后缀
|
||||
|
||||
bounds = info.get("actual_bounds", ((0, 0, 0), (0, 0, 0)))
|
||||
size_str = self._format_bounds_size(bounds)
|
||||
|
||||
lines.append(f"| {key} | `{path_short}` | {size_str} |")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 二、Phase B — 地面与边界
|
||||
# ----------------------------------------------------------
|
||||
def _write_ground_and_boundary(self) -> str:
|
||||
"""生成地面铺设和边界围挡。"""
|
||||
lines = [
|
||||
"---",
|
||||
"",
|
||||
"## 二、Phase B — 地面与边界",
|
||||
"",
|
||||
"### B1. 铺设地面",
|
||||
"",
|
||||
]
|
||||
|
||||
# 全局地面
|
||||
w, d = self.plan.scene_size
|
||||
lines.append("#### 全局地面(MI_DevGray)")
|
||||
lines.append("")
|
||||
lines.append("| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 |")
|
||||
lines.append("|---|-----------|------|-------------|------|------|")
|
||||
|
||||
# 用 Floor_1000x1000 铺满
|
||||
tiles_x = max(1, w // 1000)
|
||||
tiles_y = max(1, d // 1000)
|
||||
seq = 1
|
||||
for tx in range(tiles_x):
|
||||
for ty in range(tiles_y):
|
||||
cx = tx * 1000 + 500
|
||||
cy = ty * 1000 + 500
|
||||
lines.append(
|
||||
f"| {seq} | LVL_Global_STR_GroundFloor_{seq:02d} | Floor_1000x1000 | "
|
||||
f"({cx},{cy},{int(self.plan.ground_z)}) | — | MI_DevGray |"
|
||||
)
|
||||
seq += 1
|
||||
|
||||
lines.append("")
|
||||
|
||||
# 边界围挡
|
||||
lines.append("### B2. 放置边界围挡")
|
||||
lines.append("")
|
||||
|
||||
for boundary in self.plan.boundaries:
|
||||
seg = boundary.segment
|
||||
lines.append(f"#### {seg}")
|
||||
lines.append("")
|
||||
lines.append("| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |")
|
||||
lines.append("|---|-----------|------|-------------|-------------|------|------|")
|
||||
|
||||
# 根据边界方向生成围挡
|
||||
bnd_actors = self._generate_boundary_actors(boundary)
|
||||
for i, actor in enumerate(bnd_actors):
|
||||
rot_str = self._fmt_rotation(actor.rotation)
|
||||
scale_str = self._fmt_scale(actor.scale)
|
||||
lines.append(
|
||||
f"| {i + 1} | {actor.actor_name} | {actor.asset_key} | "
|
||||
f"{self._fmt_location(actor.location)} | {rot_str} | {scale_str} | "
|
||||
f"{actor.material} |"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _generate_boundary_actors(self, boundary: BoundaryItem) -> list[ActorSpec]:
|
||||
"""根据边界描述生成围挡 Actor。"""
|
||||
w, d = self.plan.scene_size
|
||||
actors = []
|
||||
|
||||
# 确定边界的起止坐标和方向
|
||||
if "西" in boundary.segment:
|
||||
# X=0, 沿 Y 轴
|
||||
x, y_start, y_end = 0, 0, d
|
||||
yaw = 90.0
|
||||
elif "东" in boundary.segment:
|
||||
x, y_start, y_end = w, 0, d
|
||||
yaw = 90.0
|
||||
elif "南" in boundary.segment:
|
||||
x_start, x_end, y = 0, w, 0
|
||||
yaw = 0.0
|
||||
elif "北" in boundary.segment:
|
||||
x_start, x_end, y = 0, w, d
|
||||
yaw = 0.0
|
||||
else:
|
||||
return []
|
||||
|
||||
# 使用 BarrierWall_600 或 Wall_400x300
|
||||
asset_key = "BarrierWall_600"
|
||||
wall_len = 600 # cm
|
||||
|
||||
seq = 1
|
||||
if "西" in boundary.segment or "东" in boundary.segment:
|
||||
length = y_end - y_start
|
||||
count = max(1, int(length / wall_len))
|
||||
for i in range(count):
|
||||
y_pos = y_start + i * wall_len + wall_len // 2
|
||||
z_pos = 105 # 半高
|
||||
actors.append(ActorSpec(
|
||||
actor_name=f"LVL_Global_BND_{boundary.segment}_{seq:02d}",
|
||||
asset_key=asset_key,
|
||||
asset_path="",
|
||||
location=(x, y_pos, z_pos),
|
||||
rotation=(0.0, yaw, 0.0),
|
||||
scale=(1.0, 1.0, 1.0),
|
||||
material=boundary.tag_color or "MI_DevBrown",
|
||||
material_path="",
|
||||
category="BND",
|
||||
zone_id="GLOBAL",
|
||||
folder="LVL_Level/Global/Boundaries",
|
||||
))
|
||||
seq += 1
|
||||
else:
|
||||
length = x_end - x_start
|
||||
count = max(1, int(length / wall_len))
|
||||
for i in range(count):
|
||||
x_pos = x_start + i * wall_len + wall_len // 2
|
||||
y_pos = y
|
||||
z_pos = 105
|
||||
actors.append(ActorSpec(
|
||||
actor_name=f"LVL_Global_BND_{boundary.segment}_{seq:02d}",
|
||||
asset_key=asset_key,
|
||||
asset_path="",
|
||||
location=(x_pos, y_pos, z_pos),
|
||||
rotation=(0.0, yaw, 0.0),
|
||||
scale=(1.0, 1.0, 1.0),
|
||||
material=boundary.tag_color or "MI_DevBrown",
|
||||
material_path="",
|
||||
category="BND",
|
||||
zone_id="GLOBAL",
|
||||
folder="LVL_Level/Global/Boundaries",
|
||||
))
|
||||
seq += 1
|
||||
|
||||
return actors
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 三、Phase C — 逐区域搭建
|
||||
# ----------------------------------------------------------
|
||||
def _write_zone_builds(self) -> str:
|
||||
"""生成逐区域搭建章节。"""
|
||||
lines = [
|
||||
"---",
|
||||
"",
|
||||
"## 三、Phase C — 逐区域搭建",
|
||||
"",
|
||||
]
|
||||
|
||||
for idx, zone in enumerate(self.plan.zones, 1):
|
||||
lines.append(f"### C{idx}. {zone.zone_id} — {zone.zone_name}")
|
||||
lines.append("")
|
||||
|
||||
# 结构体
|
||||
structures = self.plan.zone_structures.get(zone.zone_id, [])
|
||||
if structures:
|
||||
lines.append(f"#### C{idx}.1 结构体(Structure)")
|
||||
lines.append("")
|
||||
lines.append("| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 旋转 (P,Y,R) | 缩放 | 材质 |")
|
||||
lines.append("|---|-----------|------|-------------|-------------|------|------|")
|
||||
|
||||
for item in structures:
|
||||
actors = self.expander.expand_structure(item, zone.zone_id)
|
||||
for actor in actors:
|
||||
rot_str = self._fmt_rotation(actor.rotation)
|
||||
scale_str = self._fmt_scale(actor.scale)
|
||||
lines.append(
|
||||
f"| | {actor.actor_name} | {actor.asset_key} | "
|
||||
f"{self._fmt_location(actor.location)} | {rot_str} | {scale_str} | "
|
||||
f"{actor.material} |"
|
||||
)
|
||||
self._track_asset(actor)
|
||||
lines.append("")
|
||||
|
||||
# 布景
|
||||
props = self.plan.zone_props.get(zone.zone_id, [])
|
||||
if props:
|
||||
lines.append(f"#### C{idx}.2 布景物(Props)")
|
||||
lines.append("")
|
||||
lines.append("| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |")
|
||||
lines.append("|---|-----------|------|-------------|------|------|------|")
|
||||
|
||||
for item in props:
|
||||
actors = self.expander.expand_prop(item, zone.zone_id)
|
||||
for actor in actors:
|
||||
scale_str = self._fmt_scale(actor.scale)
|
||||
lines.append(
|
||||
f"| | {actor.actor_name} | {actor.asset_key} | "
|
||||
f"{self._fmt_location(actor.location)} | {scale_str} | "
|
||||
f"{actor.material} | {actor.note} |"
|
||||
)
|
||||
self._track_asset(actor)
|
||||
lines.append("")
|
||||
|
||||
# 组合资产(如有)
|
||||
composites = self.plan.zone_composites.get(zone.zone_id, [])
|
||||
if composites:
|
||||
lines.append(f"#### C{idx}.3 组合资产(Composite)")
|
||||
lines.append("")
|
||||
lines.append("| # | Actor 名称 | Primitives | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |")
|
||||
lines.append("|---|-----------|-----------|-------------|------|------|------|")
|
||||
for comp in composites:
|
||||
lines.append(
|
||||
f"| | ⚠️ {comp.name} | {comp.build_plan} | "
|
||||
f"(待展开) | — | — | {comp.note} |"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 四、全局物件
|
||||
# ----------------------------------------------------------
|
||||
def _write_global_objects(self) -> str:
|
||||
"""生成全局物件章节。"""
|
||||
lines = [
|
||||
"---",
|
||||
"",
|
||||
"## 四、Phase C — 全局物件",
|
||||
"",
|
||||
]
|
||||
|
||||
if not self.plan.global_objects:
|
||||
lines.append("> 无全局物件。")
|
||||
return "\n".join(lines)
|
||||
|
||||
lines.append("| # | Actor 名称 | 资产 | 位置 (X,Y,Z) | 缩放 | 材质 | 说明 |")
|
||||
lines.append("|---|-----------|------|-------------|------|------|------|")
|
||||
|
||||
for item in self.plan.global_objects:
|
||||
actors = self.expander.expand_global(item)
|
||||
for actor in actors:
|
||||
scale_str = self._fmt_scale(actor.scale)
|
||||
lines.append(
|
||||
f"| | {actor.actor_name} | {actor.asset_key} | "
|
||||
f"{self._fmt_location(actor.location)} | {scale_str} | "
|
||||
f"{actor.material} | {actor.note} |"
|
||||
)
|
||||
self._track_asset(actor)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 五、光照
|
||||
# ----------------------------------------------------------
|
||||
def _write_lighting(self) -> str:
|
||||
"""生成光照章节。"""
|
||||
lines = [
|
||||
"---",
|
||||
"",
|
||||
"## 五、Phase D — 光照",
|
||||
"",
|
||||
]
|
||||
|
||||
for light in self.plan.lights:
|
||||
if "定向" in light.light_type or "Directional" in light.light_type:
|
||||
lines.append("### D1. 定向光(Directional Light)")
|
||||
lines.append("")
|
||||
lines.append("| 类型 | 位置/方向 | 参数 |")
|
||||
lines.append("|------|---------|------|")
|
||||
lines.append(f"| Directional Light | {light.position} | {light.params} |")
|
||||
lines.append(f"> {light.note}")
|
||||
lines.append("")
|
||||
|
||||
elif "天空" in light.light_type or "Sky" in light.light_type:
|
||||
lines.append("### D2. 天空光(Sky Light)")
|
||||
lines.append("")
|
||||
lines.append("| 类型 | 位置/方向 | 参数 |")
|
||||
lines.append("|------|---------|------|")
|
||||
lines.append(f"| Sky Light | 全局 | {light.params} |")
|
||||
lines.append(f"> {light.note}")
|
||||
lines.append("")
|
||||
|
||||
elif "路灯" in light.light_type or "点光" in light.light_type:
|
||||
lines.append("### D3. 路灯点光源")
|
||||
lines.append("")
|
||||
lines.append(f"> {light.note},数量: {light.count}")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 六、验证清单
|
||||
# ----------------------------------------------------------
|
||||
def _write_checklist(self) -> str:
|
||||
"""生成验证清单章节。"""
|
||||
total = len(self.all_actors)
|
||||
zones = len(self.plan.zones)
|
||||
|
||||
lines = [
|
||||
"---",
|
||||
"",
|
||||
"## 六、Phase E — 验证清单",
|
||||
"",
|
||||
"| # | 检查项 | 状态 |",
|
||||
"|---|--------|------|",
|
||||
f"| 1 | 场景尺寸 {self.plan.scene_size[0]}×{self.plan.scene_size[1]}cm 正确 | ⬜ |",
|
||||
f"| 2 | {zones} 个区域均已搭建 | ⬜ |",
|
||||
"| 3 | 所有边界围挡无缺口 | ⬜ |",
|
||||
"| 4 | 玩家起点可到达 | ⬜ |",
|
||||
"| 5 | 所有区域可通行 | ⬜ |",
|
||||
"| 6 | 建筑入口与流线对齐 | ⬜ |",
|
||||
"| 7 | 光照方向与场景一致 | ⬜ |",
|
||||
f"| 8 | Actor 总数: {total} | ⬜ |",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
f"> 搭建指南生成完成。共 {zones} 个区域,{total} 个 Actor。",
|
||||
]
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 辅助方法
|
||||
# ----------------------------------------------------------
|
||||
def _collect_all_actors(self):
|
||||
"""收集所有区域的 Actor(用于资产映射表)。"""
|
||||
if self.all_actors:
|
||||
return # 已收集
|
||||
|
||||
# 结构体
|
||||
for zone_id, items in self.plan.zone_structures.items():
|
||||
for item in items:
|
||||
actors = self.expander.expand_structure(item, zone_id)
|
||||
self.all_actors.extend(actors)
|
||||
for a in actors:
|
||||
self._track_asset(a)
|
||||
|
||||
# 布景
|
||||
for zone_id, items in self.plan.zone_props.items():
|
||||
for item in items:
|
||||
actors = self.expander.expand_prop(item, zone_id)
|
||||
self.all_actors.extend(actors)
|
||||
for a in actors:
|
||||
self._track_asset(a)
|
||||
|
||||
# 全局物件
|
||||
for item in self.plan.global_objects:
|
||||
actors = self.expander.expand_global(item)
|
||||
self.all_actors.extend(actors)
|
||||
for a in actors:
|
||||
self._track_asset(a)
|
||||
|
||||
def _track_asset(self, actor: ActorSpec):
|
||||
"""跟踪用到的资产。"""
|
||||
if actor.asset_key and actor.asset_key not in self.asset_refs:
|
||||
try:
|
||||
from config.asset_mapping import MESH_ASSETS
|
||||
info = MESH_ASSETS.get(actor.asset_key, {})
|
||||
except ImportError:
|
||||
info = {}
|
||||
self.asset_refs[actor.asset_key] = info
|
||||
|
||||
def _level_slug(self) -> str:
|
||||
"""生成关卡标识符。"""
|
||||
name = self.plan.scene_name.replace(" ", "")
|
||||
return name
|
||||
|
||||
@staticmethod
|
||||
def _fmt_location(loc: tuple) -> str:
|
||||
"""格式化位置。"""
|
||||
return f"({int(loc[0])},{int(loc[1])},{int(loc[2])})"
|
||||
|
||||
@staticmethod
|
||||
def _fmt_rotation(rot: tuple) -> str:
|
||||
"""格式化旋转。"""
|
||||
if rot == (0, 0, 0):
|
||||
return "—"
|
||||
return f"({int(rot[0])},{int(rot[1])},{int(rot[2])})"
|
||||
|
||||
@staticmethod
|
||||
def _fmt_scale(scale: tuple) -> str:
|
||||
"""格式化缩放。"""
|
||||
if scale == (1, 1, 1):
|
||||
return "—"
|
||||
return f"({scale[0]:.2f},{scale[1]:.2f},{scale[2]:.2f})"
|
||||
|
||||
@staticmethod
|
||||
def _format_bounds_size(bounds) -> str:
|
||||
"""格式化包围盒尺寸。"""
|
||||
if not bounds or len(bounds) < 2:
|
||||
return "—"
|
||||
mn, mx = bounds[0], bounds[1]
|
||||
w = mx[0] - mn[0] if len(mx) > 0 else 0
|
||||
d = mx[1] - mn[1] if len(mx) > 1 else 0
|
||||
h = mx[2] - mn[2] if len(mx) > 2 else 0
|
||||
return f"{int(w)}×{int(d)}×{int(h)}"
|
||||
Reference in New Issue
Block a user