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 全部通过
This commit is contained in:
2026-09-17 00:45:23 +08:00
parent 47e11af7d0
commit 5581cbe05d
27 changed files with 2031 additions and 85 deletions
+64
View File
@@ -0,0 +1,64 @@
extends TestCase
## T-WIN-012:版本号展示(文档 §11)
##
## 硬性要求是「单一来源 + 可核对」:
## 版本号只允许有一处权威定义(project.godot 的 application/config/version),
## 界面从该来源读取,且必须与 project.yaml / 产物文件名 / Git tag 一致。
## 因此这里同时核对 project.godot、project.yaml、README 三处。
const AboutScreen = preload("res://src/ui/about_screen.gd")
const Credits = preload("res://src/ui/credits.gd")
func _read_text(path: String) -> String:
if not FileAccess.file_exists(path):
return ""
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
return ""
var text := file.get_as_text()
file.close()
return text
func test_version_is_semver_and_matches_project_yaml() -> void:
var version := AboutScreen.app_version()
var parts := version.split(".")
eq(parts.size(), 3, "版本号形如 X.Y.Z(实际 %s)" % version)
for part in parts:
ok(part.is_valid_int(), "版本号各段为整数(%s)" % part)
var yaml := _read_text("res://project.yaml")
ne(yaml, "", "可读取 project.yaml")
contains(yaml, "version: %s" % version, "project.yaml 的 version 与 project.godot 一致")
func test_about_manifest_lists_version() -> void:
var manifest := AboutScreen.about_manifest()
var found := {}
for item in manifest:
found[str(item["label"])] = str(item["value"])
ok(found.has("客户端版本"), "关于清单含「客户端版本」一项")
eq(found.get("客户端版本", ""), "v%s" % AboutScreen.app_version(), "清单版本号与唯一来源一致")
ok(found.has("契约版本"), "关于清单含「契约版本」")
for key in Credits.CONTRACT_VERSIONS:
contains(str(found.get("契约版本", "")), "%s v%s" % [key, Credits.CONTRACT_VERSIONS[key]], "清单含 %s 契约版本" % key)
ok(str(found.get("隐私说明", "")).contains("不存储"), "关于清单声明不存储视频画面")
func test_version_not_hardcoded_in_ui_sources() -> void:
# 反例防护:界面代码里不允许再手写一份版本号(文档 §11.2 第 1 条)
var version := AboutScreen.app_version()
for path in ["res://src/ui/about_screen.gd", "res://src/ui/main_menu.gd", "res://src/ui/game_screen.gd"]:
var text := _read_text(path)
ne(text, "", "可读取 %s" % path)
is_false(text.contains("\"%s\"" % version), "%s 未硬编码版本号字面量" % path.get_file())
func test_entry_scene_version_is_exported_into_build() -> void:
# 导出产物名必须能由版本号推导(Documentation §11.2 第 2 条)
var version := AboutScreen.app_version()
contains("opendance-windows-v%s.zip" % version, version, "产物文件名包含版本号")
ok(Credits.entry_count() >= 10, "开源声明条目仍齐备")