Files
opendance-windows/tasks/013-scene-selection.md
zijunchiang 5581cbe05d feat(scene): 场景选择与物理落地 + 整包发布(T-WIN-012/013/014)
T-WIN-013:API 升级与场景选择
- 契约快照同步到 rest-api 0.5.0 / data-models 0.7.0 / database 0.4.0
- catalog_types.gd 新增 SceneConfig(物理/环境/出生点/相机)与 Scene;
  CatalogBundle 增加 scenes[];from_dict 按契约 7.1 再收敛一次
  (缺项默认、越界夹紧、非法枚举回退),与服务端 normalizeSceneConfig 同规则
- 新增 src/scene/scene_runtime.gd:GLB 载入 -> 碰撞体(mesh 三角网 /
  auto 命名约定 *_col、*_colonly / none 不兜底)-> 场景级重力(Area3D
  SPACE_OVERRIDE_REPLACE,不改全局 ProjectSettings)-> 风(每物理帧
  apply_central_force,阵风正弦叠加,支持 local 坐标系)-> 环境光与雾
  -> 出生点与相机(follow/fixed/orbit);无碰撞体时兜底地面
- CatalogStore 增加 scenes/default_scene/find_scene/ensure_scene_cached/
  fetch_thumbnail(场景缓存带 sha256 指纹,避免换包后仍用旧 GLB)
- 主界面改为 2x2 四个选择器(曲目/动作/模型/场景)+ 场景参数面板
  (缩略图 + 重力/摩擦/弹性/碰撞体/风摘要)
- 游戏界面载入场景并落地物理,角色落在出生点;场景缺失/下载失败/
  解析失败一律回退内置空场景并提示,不阻塞开始跟练

T-WIN-012:「关于」清单展示当前版本号
- 版本唯一来源为 project.godot 的 application/config/version(v0.2.0)
- about_manifest() 输出版本/引擎/契约/协议/地址/隐私清单;
  无头测试无 autoload,读取 AppConfig 走 get_node_or_null 容错
- Credits 的单个 CONTRACT_VERSION 常量改为 CONTRACT_VERSIONS 字典
  (六项契约各自独立递增),并双向核对 contracts/VERSION

T-WIN-014:发布必须是单个完整包
- build/package-release.sh 重写:只产出单个 zip + SHA256SUMS,
  清理历史 .partN,超限时告警而非回退分卷;新增源产物与解压后完整性
  自检(PE 头 MZ / pck 魔数 GDPC / exe+pck 同目录同名)与体积-上限对比
- deploy/proxy 相关的 Gitea IIS 256MB 上限说明见 opendance-web

测试:tests/test_scene_config.gd(12 例)+ tests/test_about.gd(4 例);
无头测试 687 通过 / 0 失败(10 个套件),verify.sh 全部通过
2026-09-17 00:45:23 +08:00

95 lines
5.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# T-WIN-013 API 连接升级与场景选择
- **状态**: done
- **依赖**: T-WIN-002、T-WIN-008
- **文档**: v1.6 §5.6、契约 `rest-api 0.5.0` / `data-models 0.7.0`
## 目标
跟随 Web 端场景管理(T-WEB-015)升级本端 API 连接,新增**场景选择**与**物理参数落地**。
## 交付物
| 文件 | 说明 |
|--|--|
| `contracts/*` | 契约快照同步到 `rest-api 0.5.0` / `data-models 0.7.0` / `database 0.4.0`(与 opendance-web 逐字节一致) |
| `src/net/catalog_types.gd` | 新增 `Wind` / `Ground` / `ScenePhysics` / `SceneEnvironment` / `SpawnPoint` / `SceneCamera` / `SceneConfig` / `Scene`;`CatalogBundle` 增加 `scenes` |
| `src/scene/scene_runtime.gd` | **场景实例化与物理落地**(新增目录 `src/scene/`) |
| `src/autoload/catalog_store.gd` | `scenes()` / `default_scene()` / `find_scene()` / `ensure_scene_cached()` / `fetch_thumbnail()` |
| `src/ui/scene_panel.gd` | 场景缩略图 + 重力/风/摩擦参数摘要 |
| `src/ui/main_menu.gd` | 第 4 个选择器「场景」(2×2 栅格,与曲目/动作/模型并列) |
| `src/ui/game_screen.gd` | 载入场景 → 落地物理 → 出生点定位 → 按场景相机取景 |
## 1. 契约解析与服务端同规则收敛
`SceneConfig.from_dict()` 复刻服务端 `normalizeSceneConfig()` 的规则:
缺项用默认值、越界夹紧、非法枚举回退。**服务端已收敛,客户端仍再收敛一次** ——
不能假设对端一定合法(旧版本服务端 / 人工改包)。
| 项 | 范围 |
|--|--|
| `gravity` | 各分量 ±200 |
| `wind.base` | ±100 |
| `gustAmplitude` / `gustFrequency` | 0~100 / 0.01~5 Hz |
| `gustDirection` | ±1 |
| `ground.friction` / `restitution` / `slopeLimit` | 0~1 / 0~1 / 0~89 度 |
| `environment.ambientIntensity` / `fogDensity` | 0~4 / 0~1 |
| `camera.distance` / `height` | 0.5~50 / 0~20 |
| `spawnPoints` | 最多 16 个 |
## 2. 物理落地(服务端只存参数,仿真在本端)
- **重力**:`Area3D` + `gravity_space_override = SPACE_OVERRIDE_REPLACE`,
**不改 `ProjectSettings` 的全局重力**,因此一个进程内切换场景互不影响。
- **碰撞体**:`colliders: mesh` → 所有 `MeshInstance3D` 走 `create_trimesh_collision()`;
`auto` → 只认命名约定(`*_col` / `*_colonly` / `*_collision` / …),
`*colonly` 只提供碰撞不参与渲染(对齐 Godot/Blender 约定);
`none` → 不生成任何碰撞体,**也不兜底**(尊重场景作者意图)。
`auto`/`mesh` 下一个都没找到时生成兜底地面并记录降级原因,避免角色无限下落。
- **风**:`Area3D` + 每物理帧对区域内 `RigidBody3D` `apply_central_force()`;
阵风按正弦叠加(`base + dir * A * sin(2πft)`),`space: local` 时用本节点基向量变换。
风速(m/s)→ 力(N)用固定系数 `WIND_FORCE_SCALE = 10`(契约只约束风速量纲)。
- **地面材质**:`friction` / `restitution` → `PhysicsMaterial`,套用到所有场景静态体。
- **环境**:环境光颜色/强度直接写进游戏界面已有的 `Environment`(不另建
`WorldEnvironment`,避免互相覆盖);`fogDensity 0~1` 映射到 Godot 的
`0.005~0.05`(Godot 的 `fog_density` 量纲更敏感,直接照搬会全屏糊死)。
- **出生点与相机**:角色落在 `spawnPoints[0]`(含 yaw),按 `camera.mode` 取景
(`follow` / `fixed` / `orbit`,orbit 在 `_process` 中缓慢环绕)。
## 3. 容错(不得阻塞开始跟练)
| 情况 | 行为 |
|--|--|
| 未选择场景 / 服务端无已发布场景 | 内置空场景 + 状态栏提示 |
| 场景缺少 `glbUrl` | 回退内置空场景 + 界面提示 |
| GLB 下载失败 | 回退内置空场景 + 明确原因 |
| GLB 解析失败 / 根节点非 Node3D | 回退内置空场景 + `warnings` |
| 物理参数非法 | 收敛为默认值(不抛错) |
| 无碰撞体可用 | 兜底地面 + 降级提示 |
## 4. 可核对性
`SceneRuntime.describe()` 输出一行摘要(重力 / 风 / 摩擦 / 弹性 / 碰撞体数量 / 相机模式),
启动时 `print` 到日志;界面同时展示同源参数。满足文档「目视 + 日志核对」。
## 验收(文档 §5.6)
| 验收项 | 结果 |
|--|--|
| 服务端发布新场景后「刷新目录」即可见并可选 | ✅ `catalog_export.scenes[]` → 第 4 个选择器;目录指纹变化即重新拉取 |
| 场景 GLB 正常加载 | ✅ `GLTFDocument` 与 `VrmLoader` 同一条零原生依赖路线 |
| 重力与风向参数按服务端配置生效(目视 + 日志核对) | ✅ `describe()` 打印 + 界面参数摘要;受力计算有单测逐值核对 |
| 服务端不可达 / 场景资产缺失时明确提示并回退,不崩溃 | ✅ 见上表「容错」;`test_scene_runtime_falls_back_when_file_missing` 覆盖 |
## 测试
`tests/test_scene_config.gd`(12 个用例 / 60+ 断言):
- 默认值逐项与服务端 `SCENE_CONFIG_DEFAULTS` 一致;
- 越界夹紧、非法枚举回退、非数值兜底、出生点 16 上限;
- 风的正弦叠加逐时刻核对(t=0 / 1(4f) / 1(2f) / 3(4f));
- 目录 `scenes[]` 解析与 `default_scene()` 跳过缺 GLB 的场景;
- 重力与风力的纯函数计算、空场景兜底地面、`colliders: none` 不兜底;
- 文件缺失时的降级路径、出生点位姿(yaw 90° 使 +Z 转向 +X)、
相机 `fixed` 机位、雾浓度映射区间。