# 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` 机位、雾浓度映射区间。