chore(spec): 综合清理 + legacy-layer 迁移 spec/plan/data
主要改动: - 切到 funplay-cocos-mcp v0.5.1 (用户级配置, 项目级 .mcp.json 删除) - 仓库文档/CLAUDE.md/.gitignore 等清理过时 cocos-mcp-server 引用 - memory 文件同步: cocos-mcp-setup/path/blocker/spriteframe-uuid/prefab-persist 等加 funplay 实测警告 - memory 新建 funplay-cocos-mcp-pending-verification.md (后已被实测覆盖) - spec/plan/data: - docs/superpowers/specs/2026-09-02-legacy-layer-migration-design.md - docs/superpowers/plans/2026-09-02-legacy-layer-migration.md - docs/superpowers/data/layer-spirit-summary.json - YouleNexus: profiles.ts / defaults.ts / PlayerInfoView.prefab / scene 改动 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"_comment": "由 projects/Game_Surface_3/save/Layer*.xml (GB18030 编码) 全量解析生成。供 legacy-layer-migration 脚本逆向 gameabc.min.js 与交叉映射 YouleNexus prefab 使用。生成时间 2026-09-02。",
|
||||
"summary": {
|
||||
"total_layers": 68,
|
||||
"total_distinct_groups": 81,
|
||||
"spirit_types_found": ["0", "1", "3", "4", "5"],
|
||||
"spirit_type_distribution": {
|
||||
"0": {"layers_count": 44, "likely": "Sprite"},
|
||||
"1": {"layers_count": 38, "likely": "Text/Label"},
|
||||
"3": {"layers_count": 45, "likely": "ProgressBar"},
|
||||
"4": {"layers_count": 15, "likely": "unknown - need reverse gameabc.min.js (Particle?)"},
|
||||
"5": {"layers_count": 39, "likely": "unknown - need reverse gameabc.min.js (Button/Group?)"}
|
||||
}
|
||||
},
|
||||
"all_layers": [
|
||||
{"file": "Layer00001.xml", "id": "1", "name": "Logo_Layer", "spirits": 5, "types": ["0","1","3"], "groups": ["1","32"]},
|
||||
{"file": "Layer00002.xml", "id": "2", "name": "Login_Layer", "spirits": 18, "types": ["0","1","3","5"], "groups": ["2"]},
|
||||
{"file": "Layer00004.xml", "id": "4", "name": "MainMenu_Layer", "spirits": 32, "types": ["0","1","3","5"], "groups": ["3"]},
|
||||
{"file": "Layer00006.xml", "id": "6", "name": "Layer6", "spirits": 53, "types": ["0","1","3","4","5"], "groups": ["103","3"]},
|
||||
{"file": "Layer00007.xml", "id": "7", "name": "ewm_Layer", "spirits": 30, "types": ["1","3","4","5"], "groups": ["0","104","105"]},
|
||||
{"file": "Layer00008.xml", "id": "8", "name": "Notice_Layer", "spirits": 9, "types": ["1","3","5"], "groups": ["3","95"]},
|
||||
{"file": "Layer00009.xml", "id": "9", "name": "Feedback_Layer", "spirits": 34, "types": ["0","1","3","4","5"], "groups": ["12","42","74","98"]},
|
||||
{"file": "Layer00010.xml", "id": "10", "name": "Protol_Layer", "spirits": 4, "types": ["0","5"], "groups": ["37"]},
|
||||
{"file": "Layer00011.xml", "id": "11", "name": "ChargeInfo_Layer", "spirits": 4, "types": ["0","1"], "groups": ["20"]},
|
||||
{"file": "Layer00012.xml", "id": "12", "name": "CombatLayer", "spirits": 27, "types": ["0","1","3","4","5"], "groups": ["7","8","9"]},
|
||||
{"file": "Layer00013.xml", "id": "13", "name": "Layer13", "spirits": 6, "types": ["0","3"], "groups": ["7","8"]},
|
||||
{"file": "Layer00014.xml", "id": "14", "name": "Pay_Layer", "spirits": 43, "types": ["0","1","3","4","5"], "groups": ["53","54"]},
|
||||
{"file": "Layer00015.xml", "id": "15", "name": "JoinRoom_Layer", "spirits": 26, "types": ["0","3","4","5"], "groups": ["5"]},
|
||||
{"file": "Layer00016.xml", "id": "16", "name": "Bind_Layer", "spirits": 7, "types": ["0","1","3","5"], "groups": ["80"]},
|
||||
{"file": "Layer00017.xml", "id": "17", "name": "MenuPlayerInfo_Layer", "spirits": 15, "types": ["0","1","3","5"], "groups": ["18"]},
|
||||
{"file": "Layer00018.xml", "id": "18", "name": "Auth_layer", "spirits": 9, "types": ["0","1","3","5"], "groups": ["57"]},
|
||||
{"file": "Layer00019.xml", "id": "19", "name": "WorldRoomList_Layer", "spirits": 34, "types": ["0","1","3","4","5"], "groups": ["82","96","97"]},
|
||||
{"file": "Layer00021.xml", "id": "21", "name": "rankList_Layer", "spirits": 20, "types": ["0","1","3","5"], "groups": ["84"]},
|
||||
{"file": "Layer00022.xml", "id": "22", "name": "Share_Layer", "spirits": 6, "types": ["0","3","5"], "groups": ["11"]},
|
||||
{"file": "Layer00024.xml", "id": "24", "name": "NeighborLayer", "spirits": 7, "types": ["0","3","5"], "groups": ["88"]},
|
||||
{"file": "Layer00025.xml", "id": "25", "name": "wareHouseLayer", "spirits": 68, "types": ["0","1","3","4","5"], "groups": ["91","92","93","94"]},
|
||||
{"file": "Layer00026.xml", "id": "26", "name": "Task_Layer", "spirits": 46, "types": ["0","1","3","5"], "groups": ["10"]},
|
||||
{"file": "Layer00027.xml", "id": "27", "name": "CreateRoom_Layer", "spirits": 19, "types": ["0","1","3","5"], "groups": ["4"]},
|
||||
{"file": "Layer00028.xml", "id": "28", "name": "SeniorOptions_Layer", "spirits": 56, "types": ["0","1","3","4","5"], "groups": ["83"]},
|
||||
{"file": "Layer00029.xml", "id": "29", "name": "phoneInfo", "spirits": 38, "types": ["0","1","3","4","5"], "groups": ["106","108"]},
|
||||
{"file": "Layer00050.xml", "id": "50", "name": "MainScene_Layer", "spirits": 11, "types": ["0","1","3","4","5"], "groups": ["21"]},
|
||||
{"file": "Layer00202.xml", "id": "202", "name": "Player_Head_Score_Layer", "spirits": 60, "types": ["0","1","3","5"], "groups": ["43","44","45","46","47"]},
|
||||
{"file": "Layer00402.xml", "id": "402", "name": "GameNoticeLayer", "spirits": 2, "types": ["1","5"], "groups": ["56"]},
|
||||
{"file": "Layer00403.xml", "id": "403", "name": "Layer403", "spirits": 1, "types": ["3"], "groups": ["81"]},
|
||||
{"file": "Layer00404.xml", "id": "404", "name": "MainSceneButton_Layer", "spirits": 6, "types": ["0","1","3"], "groups": ["28"]},
|
||||
{"file": "Layer00405.xml", "id": "405", "name": "MainButton_Layer", "spirits": 6, "types": ["1","3","5"], "groups": ["21"]},
|
||||
{"file": "Layer00408.xml", "id": "408", "name": "Interact_Layer", "spirits": 21, "types": ["0","3"], "groups": ["21","39"]},
|
||||
{"file": "Layer00409.xml", "id": "409", "name": "Chat_Voice_Layer", "spirits": 20, "types": ["3","5"], "groups": ["76"]},
|
||||
{"file": "Layer00410.xml", "id": "410", "name": "Chat_Text_Layer", "spirits": 20, "types": ["1","5"], "groups": ["75"]},
|
||||
{"file": "Layer00411.xml", "id": "411", "name": "Layer411", "spirits": 6, "types": ["3","4"], "groups": ["81"]},
|
||||
{"file": "Layer00413.xml", "id": "413", "name": "btnPannelLayer", "spirits": 18, "types": ["1","3","5"], "groups": ["100","21","81","99"]},
|
||||
{"file": "Layer00416.xml", "id": "416", "name": "MainScene_PlayerInfo_Layer", "spirits": 16, "types": ["0","1","5"], "groups": ["38","77"]},
|
||||
{"file": "Layer00418.xml", "id": "418", "name": "ChatPannel_Layer", "spirits": 54, "types": ["0","1","3","5"], "groups": ["30"]},
|
||||
{"file": "Layer00420.xml", "id": "420", "name": "CheckFree_Layer", "spirits": 7, "types": ["0","1","3","5"], "groups": ["36"]},
|
||||
{"file": "Layer00601.xml", "id": "601", "name": "Layer601", "spirits": 5, "types": ["1","3","5"], "groups": ["102"]},
|
||||
{"file": "Layer00602.xml", "id": "602", "name": "inputPannel", "spirits": 19, "types": ["0","1","3","4","5"], "groups": ["87"]},
|
||||
{"file": "Layer00603.xml", "id": "603", "name": "Layer603", "spirits": 3, "types": ["0","3"], "groups": ["107"]},
|
||||
{"file": "Layer00606.xml", "id": "606", "name": "Apply_Layer", "spirits": 31, "types": ["0","1","3","4","5"], "groups": ["29","32"]},
|
||||
{"file": "Layer00607.xml", "id": "607", "name": "Cal_Layer", "spirits": 20, "types": ["0","3","4","5"], "groups": ["78","79"]},
|
||||
{"file": "Layer00608.xml", "id": "608", "name": "Help_Layer", "spirits": 7, "types": ["0","1","3","5"], "groups": ["19"]},
|
||||
{"file": "Layer00609.xml", "id": "609", "name": "Setting_Layer", "spirits": 15, "types": ["0","3","5"], "groups": ["6"]},
|
||||
{"file": "Layer00612.xml", "id": "612", "name": "Tips_Layer", "spirits": 5, "types": ["0","1","5"], "groups": ["32","89"]},
|
||||
{"file": "Layer00613.xml", "id": "613", "name": "updateRoomCard_Layer", "spirits": 3, "types": ["0","1"], "groups": ["34"]},
|
||||
{"file": "Layer00614.xml", "id": "614", "name": "Loading_Layer", "spirits": 2, "types": ["0","3"], "groups": ["40"]},
|
||||
{"file": "Layer00615.xml", "id": "615", "name": "Reconnect_Layer", "spirits": 4, "types": ["0","3"], "groups": ["35"]},
|
||||
{"file": "Layer00616.xml", "id": "616", "name": "Kick_Layer", "spirits": 3, "types": ["0","1"], "groups": ["33"]},
|
||||
{"file": "Layer00617.xml", "id": "617", "name": "iMessage_Layer", "spirits": 4, "types": ["0","1"], "groups": ["41"]},
|
||||
{"file": "Layer00618.xml", "id": "618", "name": "Layer618", "spirits": 2, "types": ["0"], "groups": ["85","86"]},
|
||||
{"file": "Layer00619.xml", "id": "619", "name": "BackHall_Layer", "spirits": 1, "types": ["3"], "groups": ["55"]},
|
||||
{"file": "Layer00620.xml", "id": "620", "name": "recordLayer", "spirits": 3, "types": ["0","3"], "groups": ["101"]}
|
||||
],
|
||||
"youle_nexus_prefab_mapping": {
|
||||
"Login_Layer.prefab": {"target_layer": "Layer00002.xml", "name": "Login_Layer", "groups": ["2"]},
|
||||
"views/PlayerInfoView.prefab": {
|
||||
"resolved": "Layer00017.xml",
|
||||
"name": "MenuPlayerInfo_Layer",
|
||||
"groups": ["18"],
|
||||
"spirits": 15,
|
||||
"decision_note": "用户拍板 A (2026-09-02)"
|
||||
},
|
||||
"templates/ListItem_Room.prefab": {"status": "NO_MATCH", "hypothesis": "ListItem_Room 可能是跨 Layer 复用的 widget 子模板(不在任何 Layer 顶层),或者是 YouleNexus 新加组件"},
|
||||
"widgets/Btn_Primary.prefab": {"status": "NO_MATCH", "hypothesis": "通用按钮 widget,可能是 YouleNexus 新加的 UI 基础组件,原项目 Layer 中以 Spirit 形式内嵌而非独立 Layer"},
|
||||
"widgets/Group106_PhoneVerify.prefab": {"target_layer": "Layer00029.xml", "name": "phoneInfo", "groups": ["106","108"]},
|
||||
"widgets/IconButton.prefab": {"status": "NO_MATCH", "hypothesis": "通用图标按钮 widget,可能 YouleNexus 新加"},
|
||||
"widgets/Layer10_Protocol.prefab": {"target_layer": "Layer00010.xml", "name": "Protol_Layer", "groups": ["37"], "spirits": 4, "types": ["0","5"]},
|
||||
"widgets/Layer609_Setting.prefab": {"target_layer": "Layer00609.xml", "name": "Setting_Layer", "groups": ["6"], "spirits": 15, "types": ["0","3","5"]},
|
||||
"widgets/Layer612_Tips.prefab": {"target_layer": "Layer00612.xml", "name": "Tips_Layer", "groups": ["32","89"], "spirits": 5, "types": ["0","1","5"]},
|
||||
"widgets/Layer613_RoomCardUpdate.prefab": {"target_layer": "Layer00613.xml", "name": "updateRoomCard_Layer", "groups": ["34"], "spirits": 3, "types": ["0","1"]},
|
||||
"widgets/Layer614_Loading.prefab": {"target_layer": "Layer00614.xml", "name": "Loading_Layer", "groups": ["40"], "spirits": 2, "types": ["0","3"]},
|
||||
"widgets/Layer615_Reconnect.prefab": {"target_layer": "Layer00615.xml", "name": "Reconnect_Layer", "groups": ["35"], "spirits": 4, "types": ["0","3"]},
|
||||
"widgets/Layer616_Kick.prefab": {"target_layer": "Layer00616.xml", "name": "Kick_Layer", "groups": ["33"], "spirits": 3, "types": ["0","1"]},
|
||||
"widgets/Layer617_Notice.prefab": {
|
||||
"resolved": "Layer00008.xml",
|
||||
"name": "Notice_Layer",
|
||||
"groups": ["3","95"],
|
||||
"spirits": 9,
|
||||
"decision_note": "用户拍板 B (2026-09-02)"
|
||||
},
|
||||
"widgets/Layer619_BackOtherGame.prefab": {"target_layer": "Layer00619.xml", "name": "BackHall_Layer", "groups": ["55"], "spirits": 1, "note": "Layer name 是 BackHall 不是 BackOtherGame,但 group 唯一匹配,spirit 数 1 表示是个简单 widget"},
|
||||
"widgets/Layer620_Record.prefab": {"target_layer": "Layer00620.xml", "name": "recordLayer", "groups": ["101"], "spirits": 3, "types": ["0","3"]},
|
||||
"widgets/Modal.prefab": {"status": "NO_MATCH", "hypothesis": "通用弹窗 widget,可能 YouleNexus 新加的 UI 基础组件"},
|
||||
"widgets/NumericLabel_Score.prefab": {"status": "NO_MATCH", "hypothesis": "数字滚动 Label widget,可能 YouleNexus 新加(用 Cocos 自定义 component 替代原 gameabc 数字渲染)"},
|
||||
"widgets/ProgressBar_Standard.prefab": {"status": "NO_MATCH", "hypothesis": "通用进度条 widget,可能 YouleNexus 新加或与某个 Sprite type=3 的 Spirit 对应"}
|
||||
},
|
||||
"open_questions": [
|
||||
"SpiritType=4 与 SpiritType=5 的语义(gameabc.min.js 逆向)",
|
||||
"Layer00007 ewm_Layer 的 group=0 表示有节点不在 group-X 父节点下(直接挂在 Layer 根)"
|
||||
],
|
||||
"out_of_scope_per_user": [
|
||||
"ListItem_Room.prefab",
|
||||
"Btn_Primary.prefab",
|
||||
"IconButton.prefab",
|
||||
"Modal.prefab",
|
||||
"NumericLabel_Score.prefab",
|
||||
"ProgressBar_Standard.prefab"
|
||||
],
|
||||
"decision_log": [
|
||||
{"date": "2026-09-02", "topic": "PlayerInfoView", "decision": "Layer00017 MenuPlayerInfo_Layer", "by": "user"},
|
||||
{"date": "2026-09-02", "topic": "Layer617_Notice", "decision": "Layer00008 Notice_Layer", "by": "user"},
|
||||
{"date": "2026-09-02", "topic": "6 个 NO_MATCH widget", "decision": "先不做(YouleNexus 新加)", "by": "user"}
|
||||
]
|
||||
}
|
||||
@@ -1465,7 +1465,7 @@ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
|
||||
1. `client.setIdentity(<真实身份>)`;监听 `open`/`login`/`message`/`slow`/`reconnecting`。
|
||||
2. `client.start()`,连真实服务器。
|
||||
3. 断言/观察:收到 `@toconcon` 不报错;发出 `player_login`;收到 `player_login` 响应且 `parseLoginResponse(...).ok === true`;console 打印 playerid/资产。
|
||||
4. 用 cocos-creator-mcp 的 `debug_get_console_logs` / `debug_screenshot` 留存联调证据。
|
||||
4. 用 funplay-cocos-mcp 的 `search_project_logs` / `capture_preview_screenshot` 留存联调证据。
|
||||
|
||||
验收标准:真实服务器返回的 `player_login` 被正确解析、`isSendLoginState` 门控正确放行、心跳不被误当业务包、断网后能重连重登。
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"
|
||||
|
||||
> **非 TDD 的验证任务,但必须排在工具开发之前。** spec §6 的三条待实测项是整个构建期合成方案的承重点;若 Auto Atlas 不在构建期打包,Task 3–6 的设计需推倒重来。先验证,再建在上面。
|
||||
>
|
||||
> **前置条件**:需要 Cocos Creator 3.8.8 打开 `YouleNexus` 且 cocos-creator-mcp 服务可用。所有编辑器操作**必须**走 `mcp__cocos-creator-mcp__cocos_*` 工具,**绝不允许手改** `.meta`/`.pac`(CLAUDE.md)。MCP 连不上时提示用户去编辑器启动服务,不得退而求其次手改文件。
|
||||
> **前置条件**:需要 Cocos Creator 3.8.8 打开 `YouleNexus` 且 funplay-cocos-mcp 服务可用(`curl http://127.0.0.1:8765/health`)。所有编辑器操作**必须**走 `mcp__funplay_cocos__*` 工具,**绝不允许手改** `.meta`/`.pac`(CLAUDE.md)。MCP 连不上时提示用户去编辑器启用扩展,不得退而求其次手改文件。
|
||||
|
||||
**Files:**
|
||||
- Create: `cocoscreator_projects/YouleNexus/assets/framework/ui/atlas-hall/probe_a.png`(验证素材,程序生成)
|
||||
@@ -290,7 +290,7 @@ Expected: 打印 `生成完成`,`atlas-hall/` 下出现两个 64×64 PNG。
|
||||
用 MCP 刷新资源,然后确认两张图各自生成了 `.meta`:
|
||||
|
||||
```
|
||||
mcp__cocos-creator-mcp__cocos_project → action: refresh_assets
|
||||
mcp__funplay_cocos__refresh_assets
|
||||
```
|
||||
|
||||
Run: `ls YouleNexus/assets/framework/ui/atlas-hall/`
|
||||
@@ -303,8 +303,8 @@ Expected: 出现 `probe_a.png.meta` 与 `probe_b.png.meta`。
|
||||
用 MCP 在 `atlas-hall/` 下创建自动图集资源(`.pac`),然后构建一次 web-mobile:
|
||||
|
||||
```
|
||||
mcp__cocos-creator-mcp__cocos_asset → 创建 atlas-hall/atlas-hall.pac(自动图集)
|
||||
mcp__cocos-creator-mcp__cocos_builder → 构建 platform=web-mobile
|
||||
mcp__funplay_cocos__write_file → 创建 atlas-hall/atlas-hall.pac(自动图集)
|
||||
mcp__funplay_cocos__run_project_preview → 构建 platform=web-mobile
|
||||
```
|
||||
|
||||
Expected(判定标准):构建产物中 `probe_a` 与 `probe_b` **被合并进同一张图集贴图**,而非各自独立的 64×64 PNG。
|
||||
@@ -1664,9 +1664,9 @@ Expected: 框架测试全部通过(原 84 个 + 本任务新增 6 个 = 90 个
|
||||
用 MCP 刷新并等待编译:
|
||||
|
||||
```
|
||||
mcp__cocos-creator-mcp__cocos_project → action: refresh_assets
|
||||
mcp__cocos-creator-mcp__cocos_debug → action: wait_compile
|
||||
mcp__cocos-creator-mcp__cocos_debug → action: get_console_logs, level: error
|
||||
mcp__funplay_cocos__refresh_assets
|
||||
mcp__funplay_cocos__run_script_diagnostics → 等待编译
|
||||
mcp__funplay_cocos__search_project_logs → query=error, regex=true
|
||||
```
|
||||
|
||||
Expected: 编译完成且无 error 级日志;`ui/theme/` 下四个 `.ts` 各生成 `.meta`。
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
**Architecture:** 分两段。**Node 侧**是纯函数转换器:读 `gameabc_*.json` → 坐标换算 / 锚点推断 / 分组 / bucket 归属 / 帧切分 → 产出 55 份中间描述 JSON + 散图。**编辑器侧**读中间描述建节点存 prefab;本计划只用 MCP 跑通一个界面验证链路,不写编辑器扩展。转换器绝不直接生成 `.prefab`/`.meta`(红线)。
|
||||
|
||||
**Tech Stack:** Node.js 20.13.1(ESM `.mjs`)、`node:test` + `node:assert`、`pngjs`(PNG 编解码,见 Global Constraints 的例外说明);编辑器侧走 `mcp__cocos-creator-mcp__cocos_*`。
|
||||
**Tech Stack:** Node.js 20.13.1(ESM `.mjs`)、`node:test` + `node:assert`、`pngjs`(PNG 编解码,见 Global Constraints 的例外说明);编辑器侧走 `mcp__funplay_cocos__*`。
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-28-legacy-ui-migration-design.md`
|
||||
|
||||
@@ -1456,7 +1456,7 @@ Expected: 中间描述 56 个、散图 1306 张、ObjectID 索引 991 条。
|
||||
|
||||
- [ ] **Step 5: 让编辑器导入散图并确认无错**
|
||||
|
||||
用 MCP 刷新资源(工具名用 `ToolSearch` 自行发现,当前扩展为 `cocos-mcp-server` 的 16 个聚合工具;刷新资源大概率是 `cocos_asset` 的某个 action,可用 `cocos_knowledge {topic:"tool_guide", query:"asset.refresh"}` 查用法)。
|
||||
用 MCP 刷新资源(直接调 `mcp__funplay_cocos__refresh_assets`;工具列表可查 `get_tool_catalog`,按返回值核对)。
|
||||
|
||||
Expected: 1306 张图被导入并各自生成 `.meta`;无 error 级日志。
|
||||
|
||||
@@ -1484,7 +1484,7 @@ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>"
|
||||
|
||||
> **非 TDD 的验证任务。** 目的是证明「中间描述 → Cocos prefab」这条链路成立,并量出落地一个界面的实际耗时,据此判断是否值得写编辑器扩展批量跑完其余 54 个。
|
||||
>
|
||||
> **前置条件**:Cocos Creator 打开 `YouleNexus` 且 cocos-mcp-server 服务可用。所有编辑器操作**必须**走 `mcp__cocos-creator-mcp__cocos_*`;**绝不允许手写** `.prefab`/`.meta`。MCP 不可用时提示用户启动服务,不得退而求其次手改文件。
|
||||
> **前置条件**:Cocos Creator 打开 `YouleNexus` 且 funplay-cocos-mcp 服务可用(`curl http://127.0.0.1:8765/health`)。所有编辑器操作**必须**走 `mcp__funplay_cocos__*`;**绝不允许手写** `.prefab`/`.meta`。MCP 不可用时提示用户去编辑器启用扩展,不得退而求其次手改文件。
|
||||
|
||||
**Files:**
|
||||
- Create: `cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab`(由编辑器生成)
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# 平台逻辑移植(子系统 C)开发计划 —— 从当前进度到「逻辑流程一致」验收
|
||||
|
||||
> **For agentic workers:** 执行时使用 `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans`,任务用 `- [ ]` checkbox 跟踪。
|
||||
> 顶层路线图:本计划定义阶段边界、依赖、验收;阶段 1 直接续用 `2026-06-28-platform-store.md`,阶段 4/5 复用 `2026-08-27-ui-asset-and-skin.md`、`2026-08-28-legacy-ui-migration.md`,阶段 2/3 为新增(执行时各自展开为 TDD 任务)。
|
||||
|
||||
**Goal:** 把子系统 C 规范(`docs/superpowers/specs/2026-08-30-legacy-platform-logic-migration.md`)的 §3 落点全部落地,使 §8 的 10 条「逻辑流程一致性检查点」全部通过。
|
||||
|
||||
**策略假设(可纠正点)**:**端到端逻辑主线优先,UI 大面积落地收尾**——先打通「启动 → 建连 → 登录 → 进房/大厅 → 对局分发」这条纯逻辑主线(Store/session/protocol/sdk),UI 用最小占位(已有 `Login_Layer.prefab`);最后再批量落地 55 界面。理由:C 规范的目标是「逻辑流程一致」而非「像素一致」,逻辑主线是协议契约与业务正确性的承重墙,UI 是响应式 Store 的下游视图、可后置。
|
||||
|
||||
**基线(已完成,勿重复)**:`core`(含 `reactive.ts`)、`net`(transport/net-client/envelope-codec/heartbeat/reconnect)、`protocol`(login + 4 rpc)、`config`(完整)、`platform`(player-store + types + readonly)、`ui/theme`。真机联调 `player_login` 往返已通过。
|
||||
|
||||
---
|
||||
|
||||
## 阶段总览(依赖图)
|
||||
|
||||
```
|
||||
阶段1 三态机+session ──┐
|
||||
├─► 阶段2 协议+收包路由 ──► 阶段3 sdk+重连 ──► 阶段5 真机联调
|
||||
阶段4 UI落地(并行) ────┘ (可与4并行) (依赖2)
|
||||
阶段5 原生桥(并行) ───────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 阶段 | 内容 | 依赖 | 产出 | 复用现有 plan |
|
||||
|---|---|---|---|---|
|
||||
| **1** | platform 三态机 + session | 无(reactive/types/player 已就绪) | AppStore/RoomStore/PlatformSession | ✅ `2026-06-28-platform-store.md` Task 4/5/6 |
|
||||
| **2** | 启动接线 + protocol 补全 + 收包路由 + 房间事件 | 阶段 1 | rpc 常量、Router、房间 handler、PlayerStore 补全 | 新写 |
|
||||
| **3** | sdk(IGameModule/GameContext)+ 断线重连 | 阶段 2 | GameContext、IGameModule、deskinfo 透传 | 新写(= 原「Plan 5」主体) |
|
||||
| **4** | UI 界面落地(55 界面 + FrameSet) | 阶段 1(响应式 Store) | FrameSet 组件、界面控制器、批量 prefab | ✅ `2026-08-28-legacy-ui-migration.md` + 新写控制器 |
|
||||
| **5** | 原生桥(WVJB)+ 真机端到端联调 | 阶段 3/4 | 完整 WVJB 桥、端到端验收 | 新写(= 原「Plan 5」收尾) |
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1:platform 三态机 + session(续现有 plan)
|
||||
|
||||
**目标**:`AppStore`(=GameData) / `RoomStore`(=Desk) / `PlatformSession`(=12_Logic 登录接入),使 login 事件 → 三 Store 正确填充 + 相位流转。
|
||||
|
||||
**直接续用** `docs/superpowers/plans/2026-06-28-platform-store.md` 的 **Task 4(AppStore)、Task 5(RoomStore)、Task 6(PlatformSession)**——该 plan 已含完整代码与测试,此处不重写,按原 TDD 流程执行。
|
||||
|
||||
- [ ] Task 4:`platform/stores/app-store.ts`(AppStore:setIdentity/setServers/setPhase/applyLogin)
|
||||
- [ ] Task 5:`platform/stores/room-store.ts`(RoomStore:applyRecovery/clear,`players` 座位数组 + `deskinfo` opaque 透传)
|
||||
- [ ] Task 6:`platform/session.ts`(PlatformSession 订阅 NetClient 事件总线 → 填充三 Store,相位流转)
|
||||
|
||||
**验收**:`npm run test:framework` 全绿(含 app-store 3 + room-store 3 + session 4 用例);`typecheck:framework` exit 0;login 含 `roomcode` 时 `RoomStore.inRoom=true` + `deskinfo` 原样透传。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 2:启动接线 + protocol 补全 + 收包路由 + 房间事件
|
||||
|
||||
**目标**:把「收包 → 路由分界 → Store 更新」这条主线打通,覆盖 C 规范 §4.1(路由分界)与 §6.2(登录分支)。
|
||||
|
||||
### Task 2.1 — protocol 补全 rpc 常量(对应 C §3 `02_Const` 落点)
|
||||
|
||||
- [ ] 从旧 `02_Const.js` 的 `RpcList`(:13–109,~100 个)提取平台层 rpc,落 `protocol/routes.ts`(扩展 `Rpc`,保持函数名==rpc 字符串的同构映射)。
|
||||
- [ ] 至少覆盖房间类 rpc(`create_room`/`self_join_room`/`self_exit_room`/`other_exit_room`/`self_makewar`/`player_prepare`/`self_apply_free_room`/`change_seat`/`update_bean`/`connect_roomserver`…)与玩家类(`update_bean`/`set_sign`/`set_tel`…)。
|
||||
- **验收**:`Rpc` 常量与 `docs/protocol/` 的 rpc 字符串逐字节一致;typecheck 通过。
|
||||
|
||||
### Task 2.2 — 收包路由分界(C §4.1 的红线)
|
||||
|
||||
- [ ] 在 `net-client.ts`(或新建 `protocol/router.ts`)实现 Router:`route ∈ {platform,agent,room}` → 平台 handler;`route = <game route>` → sdk 钩子(阶段 3 接上,本阶段先留 `onGameRoute` 空钩子 + 未注册时显式报错,不静默吞包)。
|
||||
- [ ] `connect_roomserver`/`connect_agentserver` 换服逻辑已存在(`net-client.ts:129-133`),补「换服后重发登录」的触发。
|
||||
- **验收**:单测:platform 包 → 平台 handler 被调;对局 route 未注册 → 显式报错(第二准则:不兜底)。
|
||||
|
||||
### Task 2.3 — 房间事件 handler → RoomStore/PlayerStore 更新(C §6.4)
|
||||
|
||||
- [ ] 新建 `protocol/room.ts`(或 `platform/room-handlers.ts`):`self_join_room`/`other_join_room`/`self_exit_room`/`other_exit_room`/`self_makewar`/`player_prepare`/`self/other_apply/agree/refuse_free_room`/`other_offline/online`/`change_seat` → 更新 `RoomStore`(座位、`stage`、`state`、`AgreeList`)。
|
||||
- [ ] `PlayerStore` 补全 `update_bean`/`update_roomcard`/`setCharm`/`setSign`/`setTel`(对应 C §3.1 消除双镜像:bean/roomcard 只写 `PlayerStore`,`RoomStore.players[seat]` 是同一响应式来源的投影)。
|
||||
- **验收**:单测:每个房间 rpc 收包 → RoomStore/PlayerStore 字段正确变化;解散投票 `AgreeList` 累积正确。
|
||||
|
||||
### Task 2.4 — 启动接线(C §6.1 启动流程)
|
||||
|
||||
- [ ] 把 `config/bootstrap` 的 `resolveBootstrap` 结果接 `AppStore.setIdentity/setServers`;`new PlatformSession(net.bus)` + `net.start()`。
|
||||
- [ ] 首屏门控四条件(资源/WS onopen/配置/加载计时器)合成一个「就绪信号」→ 登录页(UI 阶段 4 接,本阶段先暴露 `AppStore.phase` 状态)。
|
||||
- **验收**:启动编排单测:bootstrap → AppStore 身份/服务器就绪 → net.start 建连。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 3:sdk(IGameModule/GameContext)+ 断线重连
|
||||
|
||||
**目标**:建立子游戏对接边界,收包路由对局分支接上 sdk,`deskinfo` 透传 + 断线重连恢复(C §4.1/§5 边界判据、§6.3)。
|
||||
|
||||
### Task 3.1 — `sdk/GameContext` facade(C §3 `08_Utl_Output` 落点)
|
||||
|
||||
- [ ] 只读暴露:`room`/`player`(只读 Store)、`net.send(route,rpc,data)`、`seat.toView(mySeat,target)`(替代旧 `ChangeToStatus`)、`ui` 占位(toast/dialog)。
|
||||
- [ ] 旧 `08_Utl_Output.js` 的只读 getter(`getMyInfo`/`getRoomcode`/`getPlayerList`/`getMySeat`…)→ facade 方法或直接只读 Store。
|
||||
- **验收**:单测:facade 只读、不可逆改 Store;`seat.toView` 与旧 `ChangeToStatus` 同结果。
|
||||
|
||||
### Task 3.2 — `IGameModule` 接口 + 注册(框架 spec §4)
|
||||
|
||||
- [ ] 定义 `IGameModule`(`route`/`onEnter`/`onExit`/`onReceive`/`onReconnect`/`serialize`/平台钩子默认空实现)。
|
||||
- [ ] 注册机制:子游戏注册自己,Router 据 `route` 分发对局包 → `activeGame.onReceive`。
|
||||
- **验收**:单测:注册 mock 子游戏 → 对局 route 包 → `onReceive` 被调;未注册 → 显式报错。
|
||||
|
||||
### Task 3.3 — `deskinfo` 透传 + 断线重连(C §6.3)
|
||||
|
||||
- [ ] login 回包含 `deskinfo`(`isbattle==1`)→ `RoomStore` 只透传不解析 → `activeGame.onReconnect(deskinfo)`。
|
||||
- [ ] 断线重连完整链路:`net/reconnect` 重连 → 重发登录 → `deskinfo` 恢复对局。
|
||||
- **验收**:集成测试:login(deskinfo) → `onReconnect` 收到原样快照;断线 → 重连 → 重发登录。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 4(并行):UI 界面落地
|
||||
|
||||
**目标**:从 55 界面中间描述批量落地 Cocos 界面,响应式订阅 Store,界面切换走 `node.active` + `AppStore.currentScene`(C §6.5)。
|
||||
|
||||
- [ ] **Task 4.1 FrameSet 组件**(B §3.4):多帧图切帧的运行时载体(`setFrame(n)`,1 基帧号)。
|
||||
- [ ] **Task 4.2 界面控制器**:登录/大厅/房间 + 各弹窗,`subscribe` 三 Store 自动刷新;`Login_Layer.prefab` 接入真实登录数据。
|
||||
- [ ] **Task 4.3 批量落地**:复用 `2026-08-28-legacy-ui-migration.md` 的 convert 管线,把 55 个 layer JSON → prefab(当前仅 Login_Layer 已落)。
|
||||
- [ ] **Task 4.4 界面切换**:`AppStore.currentScene` 状态替代 `get_self(149,37)` 哨兵。
|
||||
|
||||
**验收**:登录→大厅→房间 界面切换正确;帧动画走 FrameSet;UI 由 Store 响应式驱动(无手工刷新)。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 5(并行收尾):原生桥 + 真机端到端联调
|
||||
|
||||
- [ ] **Task 5.1 WVJB 完整异步桥**(原「Plan 5」):`window.settings` 同步取值 + `setupWebViewJavascriptBridge` 异步注册,接口名/数据格式/URL 构造与旧逐字一致(`native-bridge-contract` 技能)。
|
||||
- [ ] **Task 5.2 真机端到端**:登录 → 大厅 → 进房 → 对局(mock/seed 子游戏)→ 结算,联调本地服。
|
||||
- [ ] **Task 5.3 断线重连真机验证**:`isbattle==1` 时 `deskinfo` 恢复对局。
|
||||
|
||||
---
|
||||
|
||||
## 验收标准(映射 C 规范 §8 检查点)
|
||||
|
||||
- [ ] 信封 `{app,route,rpc,data}` 逐字节对齐(✅ 已有,回归不破坏)
|
||||
- [ ] 收包分界:`platform/agent/room` → 平台 handler;对局 route → `onReceive`;未注册显式报错(阶段 2/3)
|
||||
- [ ] 发包 HTTP 兜底(`netType==1`)+ `player_login` 的 `putMsg="playerLogin"`(阶段 2)
|
||||
- [ ] 启动门控四条件合成就绪信号(阶段 2)
|
||||
- [ ] 登录分支:`roomcode`/`deskinfo` 有无 → 大厅 or 房间 or `onReconnect`(阶段 1/3)
|
||||
- [ ] 断线重连 + `deskinfo` 恢复(阶段 3/5)
|
||||
- [ ] 三态机单一来源,无双镜像(阶段 1/2)
|
||||
- [ ] 对局态 `deskinfo` 平台层不解析、原样透传(阶段 3)
|
||||
- [ ] 界面切换 `node.active` + `currentScene`,不搬 `set_level`(阶段 4)
|
||||
- [ ] 原生接口逐字一致(阶段 5)
|
||||
|
||||
---
|
||||
|
||||
## 风险与依赖
|
||||
|
||||
1. **UI 落地量大**(55 界面、旧 9862 行)——通过「逻辑主线先行 + 响应式 Store 后置 UI」隔离,UI 延迟不阻塞阶段 2/3 的协议正确性验证。
|
||||
2. **对局 route 的 sdk 分界是红线**——阶段 2.2 先留「未注册显式报错」,避免静默吞包(第二准则)。
|
||||
3. **`set_windows`/`players` 完整建模**等次要项——按 C §9.3 待验证清单在对应阶段落地时回填,不阻塞主线。
|
||||
4. **换肤/打包(ui-asset-skin plan)**——独立于本计划,需在阶段 4 UI 落地时同步执行(散图 → Auto Atlas 打包),避免 UI 落地后返工。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -84,7 +84,7 @@ A 与 B 互不依赖、可并行;C 必须等两者。
|
||||
|
||||
### 0.4 已确认决策
|
||||
|
||||
- **设计分辨率保持 1280×720 + `fitHeight`** —— 991 个对象的 X/Y **全部 1:1 迁移**,不做任何坐标缩放。现代宽屏由 `fitHeight` 天然获得(20:9 下水平可见区约等于 1600×720)。
|
||||
- **设计分辨率设为 1600×720(20:9 主流移动端比例)+ `fitHeight`** —— 991 个对象的 X/Y **全部 1:1 迁移**,不做任何坐标缩放。旧引擎的 1280×720 降格为**内容区基准**(`DESIGN_WIDTH`),只用于坐标换算、三区锚点推断、满屏/越界判断;设计分辨率画布宽度是**另一个独立常量**(`CANVAS_WIDTH = 1600`),落地时据此设定 Layer/Group 根节点尺寸。20:9 实机 1:1 无缩放,16:9 老设备由 `fitHeight` 收缩画布、Widget 弹性贴边自动适配。
|
||||
- **多帧图切成散图**(1306 张)—— 多帧网格是旧引擎缺图集能力的手工替代品,Cocos 的 Auto Atlas 在构建期自动打包;照搬它等于把引擎缺陷当成新框架的约定。
|
||||
- **同组对象挂同一父节点** —— `GroupList` 覆盖 990/991,是旧引擎用来做整体操作的机制,语义等价于父节点。
|
||||
- **锚点按中心点三区自动推断**,仅水平方向(§2.3)。
|
||||
@@ -208,9 +208,9 @@ tools/ui-migration/intermediate/
|
||||
解法是让父节点**不产生坐标偏移、且与屏幕同尺寸**:
|
||||
|
||||
```
|
||||
Layer 根节点 1280×720,Widget 四边拉伸
|
||||
└─ Group 节点 1280×720,Widget 四边拉伸,position (0,0)
|
||||
└─ 对象节点 §2.2 转换后的绝对坐标
|
||||
Layer 根节点 1600×720(设计分辨率画布),Widget 四边拉伸
|
||||
└─ Group 节点 1600×720,Widget 四边拉伸,position (0,0)
|
||||
└─ 对象节点 §2.2 转换后的绝对坐标(以 1280 内容区基准换算)
|
||||
```
|
||||
|
||||
于是子对象坐标 = 转换后的绝对坐标(无需再减父偏移),其 Widget 锚定的满屏父节点等价于锚定屏幕边界。
|
||||
@@ -224,13 +224,15 @@ x = Left + Width / 2 − 640
|
||||
y = 360 − Top − Height / 2
|
||||
```
|
||||
|
||||
其中 `640`/`360` 是**内容区基准 1280×720 的一半**(`DESIGN_WIDTH / 2`),不是设计分辨率画布 1600 的一半。对象中心 `x = 0` 落在内容区中心,而 Layer/Group 根节点是 1600×720,两者中心重合于屏幕中心——所以居中对象天然居中,靠边对象再由 Widget 贴到画布边缘,无需为画布变宽重算坐标。
|
||||
|
||||
**为何不用 `(0, 1)`(左上)**:那样公式更简(`x = Left − 640`、`y = 360 − Top`)且与旧引擎语义一一对应,但用户明确会**在编辑器里人工审核并修改节点树**,而 Cocos 的编辑器、对齐工具、Widget 全部假定锚点居中。转换公式复杂一点是转换器一次性的成本,人工编辑的别扭是长期成本。
|
||||
|
||||
原始 `Left`/`Top` 存入 `legacy`,回溯对照随时可查。
|
||||
|
||||
### 2.3 锚点推断:只需要水平方向
|
||||
|
||||
**`fitHeight` 下垂直方向永远精确撑满、不会溢出,因此不需要任何垂直适配**——垂直用固定位置即可。这是保持 1280×720 设计分辨率带来的实质简化。
|
||||
**`fitHeight` 下垂直方向永远精确撑满、不会溢出,因此不需要任何垂直适配**——垂直用固定位置即可。这是设计分辨率高度保持 720(与内容区基准高度一致)带来的实质简化。
|
||||
|
||||
水平方向按对象中心点 `cx = Left + Width / 2` 三区推断:
|
||||
|
||||
@@ -240,7 +242,7 @@ y = 360 − Top − Height / 2
|
||||
| `cx > 960` | 靠右 | `right = 1280 − (Left + Width)` |
|
||||
| 其余 | 水平居中 | `horizontalCenter = cx − 640` |
|
||||
|
||||
分界取 1280 的四等分点。这是**机械规则,不追求完美**——用户已确认转换后会逐屏人工审核修正,规则只需「大部分对、且错了容易看出」。
|
||||
分界取**内容区基准 1280** 的四等分点(与设计分辨率画布 1600 无关;`cx`/`Left`/`right` 里的 `1280`/`640` 均指内容区基准)。这是**机械规则,不追求完美**——用户已确认转换后会逐屏人工审核修正,规则只需「大部分对、且错了容易看出」。
|
||||
|
||||
### 2.4 背景:四边拉伸
|
||||
|
||||
@@ -383,6 +385,8 @@ gameabc_*.json ──[Node 转换器]──► 中间描述 55 份 + 散图 1306
|
||||
| 3 | MCP 批量建节点的可行性与耗时(991 节点) | 实测(2026-08-29):`Login_Layer` 18 节点落地共 **76 次 MCP 调用、约 20 分钟**。`builder.build` 1 次可建整棵树,但 cc.Widget「flag 使能即采纳当前 margin」会按瞬时尺寸污染 margin,需**逐节点 `set_property` 修正**(约 22 次、每次 ~3.5s)。按 991 节点外推仅 margin 修正即需 ~1000 次调用(≈58 分钟纯往返)。**结论:逐节点 MCP 不可行** | 已确认:修 builder 的 Widget 赋值时序(margin 在最终尺寸后设)或写编辑器扩展批量修正,二者取一 |
|
||||
| 4 | 三区锚点规则的实际命中率 | 抽样 5 个界面视觉比对 | 调整分界阈值,或改为逐元素人工标注 |
|
||||
|
||||
> **margin 污染的事后修复方法(2026-08-30 实测确认)**:cc.Widget 四边拉伸下 `contentSize` 是**派生值**(`= 父尺寸 − left − right`),不是自由变量。修正时**必须 `set_property` 设公开属性 `left`/`right`**(触发 setter 正向对齐,`contentSize` 自动跟随);**切勿 set `_contentSize`**——那会触发 Widget 反算,把刚设好的 margin 打回污染值(`-160`),形成死循环(实测:set contentSize=1600 → 实际返回 1920 且 margin 弹回 -160)。正确路径实测:先恢复 `_enabled=true`,再设 `left=0`、`right=0`,`contentSize` 自动从 1920 收敛到 1600。已落地样板:`should_hide_in_hierarchy`(`left/right=0 + contentSize=1600 + originalWidth=0`),而 `Login_Layer`/`group-2` 污染时 `originalWidth=100`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 非目标(YAGNI)
|
||||
@@ -399,6 +403,6 @@ gameabc_*.json ──[Node 转换器]──► 中间描述 55 份 + 散图 1306
|
||||
## 7. 与其它 spec 的接口
|
||||
|
||||
- **`2026-08-27-ui-asset-and-skin-design.md`**:切出的散图落入其定义的 8 个可覆盖目录;bucket 划分沿用其 §2 规则 3 的目录表;label 样式取自其 §4 的 `theme.fonts`。
|
||||
- **子系统 B(引擎机制映射,待写)**:需要 A 产出的 `legacy.objectId` / `groupId` / `events`;其产出的属性号对照表是 C 的前置。
|
||||
- **子系统 C(平台逻辑移植,待写)**:依赖 A 的 ObjectID → 节点路径全局索引(§1.3)与 B 的 API 映射表。
|
||||
- **子系统 B(引擎机制映射)→ `2026-08-30-legacy-engine-api-mapping.md`**:需要 A 产出的 `legacy.objectId` / `groupId` / `events`;其产出的属性号对照表是 C 的前置。
|
||||
- **子系统 C(平台逻辑移植)→ `2026-08-30-legacy-platform-logic-migration.md`**:依赖 A 的 ObjectID → 节点路径全局索引(§1.3)与 B 的 API 映射表;并定义四层架构映射、状态归属边界、核心流程一致性检查点。
|
||||
- **组件替换 spec(待写)**:`SeatView` / 按人数布局。牌桌内的 `Player_Head_Score_Layer`(60 个对象)由 A 转出静态版本后,该 spec 决定哪些改为按人数动态生成。
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
# 旧 gameabc 渲染/精灵 API → Cocos 映射(子系统 B·渲染子集)
|
||||
|
||||
> 状态:已与用户确认。
|
||||
> 适用工程:`cocoscreator_projects/YouleNexus`(框架真源)。
|
||||
> 数据源:`projects/Game_Surface_3/js/gameabc.min.js`(逐行逆向,行号见各节引用)。
|
||||
> 关联:`2026-08-28-legacy-ui-migration-design.md`(子系统 A)——帧序/切帧/`FrameSet`/图集命名 的**权威定义在 A 的 §0.3、§3.2、§3.4**,本文只引用、不复写。
|
||||
> 范围:子系统 B 中「渲染与精灵」相关的一小撮 `ifast_*` API + 支撑它们的 `set_self`/`get_self` 属性号 + 图片资源映射。**不含**网络(`ifast_ws`/`ifast_tcp_*`)、输入(`ifast_input`)、工具(`ifast_split` 等)——那些归 B 的后续章节,另立。
|
||||
|
||||
---
|
||||
|
||||
## 0. 最高原则(两条并列,贯穿全文)
|
||||
|
||||
旧引擎是**单 Canvas 立即模式**:每个精灵每帧把它的图 `drawImage` 到共享画布,`ifast_*` 都是「即时画一笔」。Cocos 是**保留式场景图 + 组件**:节点建一次,改值只改属性。
|
||||
|
||||
> **原则一 · 原理等价**:把「每帧即时绘制」翻译成「一次性建节点/挂组件 + 按需改属性」;能用一个现成组件的,绝不搬旧引擎的手工绘制。
|
||||
|
||||
> **原则二 · 表现等效,原理自由**:**一律采用 Cocos 高效、现代化、行业标准的处理方式和解决方案,不受旧引擎实现原理的束缚。** 只要做到**等效旧引擎的表现**即可——原理、处理方式可以完全不同。旧引擎的手工做法(逐位画数字、手算进度条源宽、手动 `spaceY*i` 排列表项)是「无引擎能力」的替代品,Cocos 有原生组件时**必须用原生组件**,不照搬替代品。
|
||||
|
||||
本文 §5 的每一条映射都给出「平替」与「更优(Cocos 行业标准)」两档,落地**默认取更优档**;「平替」仅在原生组件确实不适用时作为兜底。典型落实:进度条用 `ProgressBar`(§5.1)、图片数字用 `Label`+BMFont(§5.4)、列表用 `Layout`+`ScrollView`+`NodePool`(§5.5)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 渲染模型差异(这是所有映射的根)
|
||||
|
||||
| 维度 | 旧引擎(gameabc) | Cocos Creator 3.8 |
|
||||
|---|---|---|
|
||||
| 渲染方式 | 立即模式,每帧重画全部精灵 | 保留式,场景图 + 组件,脏标记局部重绘 |
|
||||
| 精灵本体 | `GameObject`(`gameabc.min.js:1534`),字段全平铺 | `cc.Node` + 组件(`Sprite`/`Label`/`UIOpacity`…) |
|
||||
| 定位 | `obj.x/y/w/h`(左上原点,Y 向下) | `node.position` + `UITransform.contentSize`(中心原点,Y 向上) |
|
||||
| 图片 | `obj.image`(HTML `Image`),按 `recid` 从 `ImageFileList` 取 | `Sprite.spriteFrame`(`SpriteFrame`,来自 Auto Atlas) |
|
||||
| 文字 | `obj.caption`,引擎 `fillText` 画 | `Label.string` |
|
||||
| 显隐 | `obj.visbale` | `node.active` |
|
||||
| 帧动画 | `obj.frame`(1 基帧号),画对应源矩形 | `FrameSet.setFrame(n)`(A §3.4) |
|
||||
| 「精灵上再画一张图」 | `ifast_mydrawbmp` 即时叠加 | **子 `Sprite` 节点**(一个 Sprite 只能一张图) |
|
||||
|
||||
坐标系换算(左上→中心)不是本文职责,权威公式在 A §2.2;本文只负责「API 语义 → Cocos 等价物」。
|
||||
|
||||
---
|
||||
|
||||
## 2. `GameObject` ↔ `cc.Node`(数据结构映射)
|
||||
|
||||
`GameObject`(`gameabc.min.js:1534-1719`)是一个大而全的平铺对象;`ifast_getobj(spid)`(`:3086` / `:3888`)返回 `gameabc_Object["sp"+id]`(`id<1` 时是画布根 `gameabc_face.obj`)。**`spid` 就是 `ObjectID`**,与 A 的 `legacy.objectId` 同一体系。
|
||||
|
||||
对应关系(只列渲染相关,其余字段见 §3 属性号):
|
||||
|
||||
| GameObject 字段 | 含义 | Cocos |
|
||||
|---|---|---|
|
||||
| `x` / `y` | 左上坐标 | `node.position`(注意坐标变换,见 A §2.2) |
|
||||
| `w` / `h` | 显示宽高 | `UITransform.width / height` |
|
||||
| `image` | 当前 HTML `Image` | `Sprite.spriteFrame` |
|
||||
| `recid` | 图片资源号 | `SpriteFrame` 的来源(§4) |
|
||||
| `caption` | 文字 | `Label.string` |
|
||||
| `frame` | 当前帧号(1 基) | `FrameSet`(A §3.4) |
|
||||
| `visbale` | 可见 | `node.active` |
|
||||
| `canclick` | 可点击 | `Button.interactable` |
|
||||
| `z_Order` / `z_index` | 绘制序 | 节点树 sibling 序(A 用 `siblingIndex`) |
|
||||
| `scale` / `ap` | 缩放 / 透明度 | `node.scale` / `UIOpacity.opacity`(§3) |
|
||||
|
||||
> 命名陷阱(重要):`ImageFileList[recid]` 里的 `w`/`h` 是**网格列数/行数**,`w1`/`h1` 是**每帧像素宽高**(见 `gameabc_getrect2` `:3958-3961`);而 `gameabc_Object["sp"+id]` 里的 `w`/`h` 是**精灵显示宽高**。两个对象字段同名、含义不同,写映射代码时**必须**分清「资源元数据」和「精灵实例」。
|
||||
|
||||
---
|
||||
|
||||
## 3. `get_self` / `set_self` 属性号对照表
|
||||
|
||||
`set_self(id, cpid, val, mode, val2)`(`:3261`)与 `get_self(id, cpid, val, mode, val2)`(`:3154`)是平台层 1773 处调用的通用读写。`cpid` 是数字属性号,`mode` 经 `gamabc_getval(val, val2, mode)`(`:3895`)决定 `val2` 如何作用到当前值:
|
||||
|
||||
| mode | 语义 | 说明 |
|
||||
|---|---|---|
|
||||
| 0 | 直接赋值(`set`)/ 读原值(`get`,内部转 `-1`) | 最常用;三参调用 `set_self(id,cpid,val)` 默认 mode=0 |
|
||||
| 1 | `当前 + val2` | 相对位移 |
|
||||
| 2 | `当前 − val2` | 相对位移 |
|
||||
| 3 | `⌊当前 × val2⌋` | 相对缩放 |
|
||||
| 4 | `⌊当前 ÷ val2⌋` | 相对缩放 |
|
||||
|
||||
**mode 实证结论(2026-08-30)**:
|
||||
- 全平台层 1773 处 `set_self` 中,**只有 mode=1(加法)被使用**,约 27 处(21 处 x/y 坐标相对位移 + 6 处 caption 的 mode 无效);`mode=2/3/4`(减/乘/除)**零使用**;`get_self` 亦无 mode 相对运算。
|
||||
- mode=1 全部是坐标相对位移:`set_self(spid, 18/19, offmovex/offmovey, 1, 0)` → `node.x += offmovex`。**C 侧无需专门相对运算辅助方法**,直接 `node.x += off` 即可。
|
||||
- ⚠️ **`set_self` 的 `switch` 分两类**:`case 6/7/16/28/58` 是**直接赋值、不走 `gamabc_getval`**,它们的 `mode`/`val2` 参数**无效**。最典型是 `set_self(spid, 7, text, 1, 0)`(caption)——那个 `1` 是历史冗余,与 `set_self(spid, 7, text)` 等价。C 移植时**只对「走 `gamabc_getval` 的 case」关心 mode**,其余一律当直接赋值。
|
||||
|
||||
属性号 → Cocos 映射(**✅ 已实证** = 由 `get_self`/`set_self` 的 `switch` 逐 case 确认;**⚠️ 推断** = 由字段默认值/命名推断,落地前须确认;**❓ 待逆向** = 语义未明,不得臆测,C 移植时按需补齐):
|
||||
|
||||
| cpid | 旧语义 | 读 | 写 | Cocos | 置信 |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 图片资源号 `recid` | `gamabc_getval(obj.recid,…)` | 设 `recid` 并触发 `gameabc_load_img2` 换 `image`(`:3311-3320`) | `sprite.spriteFrame = 帧`(recid→帧见 §4) | ✅ |
|
||||
| 7 | 文字 `caption` | 返回 `caption`,或 `measureText` | `val.toString()` 存入 `caption`(`:3297-3309`) | `label.string` | ✅ |
|
||||
| 18 | 坐标 x | `obj.x` | `obj.x` | `node.x`(经 A §2.2 换算) | ✅ |
|
||||
| 19 | 坐标 y | `obj.y` | `obj.y` | `node.y` | ✅ |
|
||||
| 20 | 宽 w | `obj.w` | `obj.w` | `UITransform.width` | ✅ |
|
||||
| 21 | 高 h | `obj.h` | `obj.h` | `UITransform.height` | ✅ |
|
||||
| 28 | 矩形 `{x,y,w,h}` | 返回四值对象 | 一次设四值(`:3279-3286`) | 同时设 `position` + `contentSize` | ✅ |
|
||||
| 37 | 可见 `visbale` | `obj.visbale` | `obj.visbale` | `node.active` | ✅ |
|
||||
| 43 | 当前帧 `frame`(1 基) | `obj.frame` | `obj.frame` | `FrameSet.setFrame(n)` | ✅ |
|
||||
| 44 | 分组 `groupid` | `obj.groupid` | `obj.groupid` | A §2.1 已并入父节点,无需单独映射 | ✅ |
|
||||
| 58 | 颜色 `color` | `obj.color` | `obj.color` | `Label.color` / `Sprite.color`(按节点类型) | ✅ |
|
||||
| 33 | 缩放 `scale`(百分比,100=原始) | `obj.scale` | `obj.scale` | `node.scale`(`= val / 100`) | ✅(`gameabc_charge:4963-4967` `ctx.scale(val/100)`) |
|
||||
| 41 | 可点击 `canclick` | `obj.canclick` | `obj.canclick` | `Button.interactable` | ✅ |
|
||||
| 16 | 引用 `f`(父/关联对象) | `obj.f` | `val<=0→0`,否则 `ifast_getobj(val)` | 节点引用(父节点 `node.parent` 或业务引用) | ⚠️ 推断(引用语义,具体用途待 C 侧确认) |
|
||||
| 6 | 图片对象 `image`(底层) | `obj.image` | 直接塞 HTML `Image` | 一般不用;用 cpid=1 换 `spriteFrame` 即可 | ✅(但少用) |
|
||||
| 35 | 透明度 `ap`(0–255,默认 255) | `obj.ap` | `obj.ap` | `UIOpacity.opacity` | ✅(`gameabc_charge:4949-4956` `globalAlpha=ap/255`;**平台层实际使用**:按钮按下/选中态) |
|
||||
| 34 | 旋转 `arge`(度,绕中心) | `obj.arge` | `obj.arge` | `node.angle` | ✅(`gameabc_charge:4944-4946` `rotate(arge·π/180)`;引擎已实现,本项目无调用) |
|
||||
| 36 | `hu` | `obj.hu` | `obj.hu` | **忽略** | ✅(引擎**空字段**:`gameabc_charge:4962` `if(obj.hu!=0){}` 无任何作用) |
|
||||
| 45 / 46 | 变换中心偏移 `cx` / `cy`(像素,默认 0) | `obj.cx` / `obj.cy` | 同左 | 调整 `anchorPoint`(≈ `(0.5+cx/w, 0.5−cy/h)`,注意 Y 向) | ✅(`gameabc_toclient:4926-4930` `translate(w/2+cx, h/2+cy)`;本项目无调用) |
|
||||
| 51 | 镜像 `jx`:1=垂直翻转、2=水平翻转 | `obj.jx` | `obj.jx` | `node.scale.y=-1` / `node.scale.x=-1` | ✅(`gameabc_charge:4969-4980` `scale(1,-1)`/`scale(-1,1)`;本项目无调用) |
|
||||
| 57 | 定时器间隔 `ontime`(毫秒,0=停) | `obj.ontime` | 设 `ontime`,为 0 时清 `click_time` | `component.schedule(cb, ms/1000)` / `unschedule` | ✅(`drawone:1878-1898` 触发 `ontimer`/`ontimer_<objid>`;**平台层实际使用**:登录等待/VIP 刷新/倒计时) |
|
||||
|
||||
> **逆向结论(2026-08-30)**:上表属性号已全部逆向。其中 `arge`(34) / `hu`(36) / `cx`·`cy`(45/46) / `jx`(51) 引擎**实现了渲染逻辑但本项目未用**——`gameabc_Object.json` 静态数据无这些字段、平台层与子游戏层也**没有** `set_self`/`get_self` 调用(已 grep 全量确认)。属「引擎预留能力」,C 移植只需在碰到调用时按上表映射即可,无需专门适配。真正仍属「未定」的只有 `default` 分支的 `other[cpid]`(业务自定义属性,按需在 `gameabc.min.js` 对应处补齐后回填)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 图片资源映射:`recid` / `ImageFileList` → `SpriteFrame`
|
||||
|
||||
旧引擎按资源号 `recid` 从 `gameabc_Image.ImageFileList[recid]` 取图(`gameabc_load_img2` `:4597`),图可能是单帧或多帧网格。A 已把多帧图切成散图、单帧图原样保留,并约定:
|
||||
|
||||
- 资源 `recid` 的多帧图 `00014.png`(3×4)→ 散图 `00014_01.png … 00014_12.png`(**1 基、补零对齐字典序**,A §3.2)
|
||||
- 散图按 bucket 落进 `framework/ui/atlas-*`,构建期由 Auto Atlas 打包(A §3.3)
|
||||
- 每张散图在 Cocos 里就是一张 `SpriteFrame`
|
||||
|
||||
**帧号 ↔ 源矩形** 的换算(用于把 `ifast_mydrawbmp` 的 `(bmp_x, bmp_y, bmp_w, bmp_h)` 反推成帧号):
|
||||
|
||||
```
|
||||
帧宽 w1 = ImageFileList[recid].w1;帧高 h1 = .h1
|
||||
网格列数 cols = .w;网格行数 rows = .h
|
||||
帧号 frame(1 基)→ 源矩形:
|
||||
列 = (frame − 1) % cols,行 = ⌊(frame − 1) / cols⌋
|
||||
源矩形 = (列 × w1, 行 × h1, w1, h1)
|
||||
|
||||
源矩形 → 帧号(逆运算,供 mydrawbmp 反推):
|
||||
列 = bmp_x / w1,行 = bmp_y / h1
|
||||
frame = 行 × cols + 列 + 1
|
||||
```
|
||||
|
||||
权威公式在 A §0.3(已实证:通知=10、设置=11、战绩=4、背包=5、仓库=6、任务=3 六项全命中)。本文复述仅为自洽,**改动以 A 为准**。
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心 API 逐条映射
|
||||
|
||||
### 5.1 `ifast_mydrawbmp(spid, recid, sp_x, sp_y, sp_w, sp_h, bmp_x, bmp_y, bmp_w, bmp_h)` — 在精灵上叠加绘制图片
|
||||
|
||||
**旧语义**(`:2627`):在精灵 `spid` 上、其坐标系内偏移 `(sp_x, sp_y)` 处、以 `(sp_w, sp_h)` 尺寸,叠加绘制图 `recid` 的子矩形 `(bmp_x, bmp_y, bmp_w, bmp_h)`。**不动 spid 自己的图**。`bmp_w <= 0` 时退化为 `ifast_drawtext`(画文字,§5.2)。
|
||||
|
||||
关键证据:`gameabc_load_img2(recid)` 加载的是 **recid** 的图,`drawImage(img, bmp_x, …, obj.x + sp_x, obj.y + sp_y, sp_w, sp_h)`(`:2639`)——坐标加的是 spid 偏移,spid 的 `image` 全程未改。
|
||||
|
||||
**Cocos 对应**:保留式场景图里一个 `Sprite` 只能显示一个 `spriteFrame`,所以「精灵之上再叠一张图」不能合并到同一 Sprite,**必须挂一个子 `Sprite` 节点**:
|
||||
|
||||
```
|
||||
spid (Sprite,自己的 spriteFrame 不变)
|
||||
└─ childSprite spriteFrame = 子矩形对应的 SpriteFrame(§4 反推帧号)
|
||||
position = (sp_x, sp_y)
|
||||
size = (sp_w, sp_h)
|
||||
```
|
||||
|
||||
```ts
|
||||
// 伪代码(Cocos 3.8)
|
||||
function myDrawBmp(spid: Node, recid: number,
|
||||
spX: number, spY: number, spW: number, spH: number,
|
||||
bmpX: number, bmpY: number, bmpW: number, bmpH: number) {
|
||||
const frame = rectToFrame(recid, bmpX, bmpY, bmpW, bmpH); // §4 逆运算
|
||||
const child = new Node('drawbmp');
|
||||
child.setPosition(spX, spY);
|
||||
const ui = child.addComponent(UITransform);
|
||||
ui.setContentSize(spW, spH);
|
||||
const sprite = child.addComponent(Sprite);
|
||||
sprite.spriteFrame = loadFrame(recid, frame); // 散图 SpriteFrame
|
||||
spid.addChild(child);
|
||||
}
|
||||
```
|
||||
|
||||
**两种典型动态用法 → 更优方案**:
|
||||
|
||||
| 旧用法 | 旧写法 | Cocos 更优解 |
|
||||
|---|---|---|
|
||||
| **进度条**(子矩形宽随进度变,`11_GameUI.js:2912`) | `ifast_mydrawbmp(spid,96,…, bmp_w = LoadCount*416/100, …)` | `cc.ProgressBar` + `Sprite.FillType.FILLED`/`SLICED`,设 `progress∈[0,1]`,**不手算源宽** |
|
||||
| **动态图标/帧片段**(`11_GameUI.js:2896` 画 47 的两个子矩形) | 多次 `ifast_mydrawbmp` 叠加 | 多个子 `Sprite`(语义等价),或若就是换帧则用 `FrameSet.setFrame`(A §3.4) |
|
||||
|
||||
> 判断「是叠加还是换帧」:看目标精灵 `spid` 自己有没有图。若 `spid` 自身无图、`mydrawbmp` 是唯一内容,等价于「给 spid 换帧」,用 `FrameSet`/`spriteFrame`;若 `spid` 有图、`mydrawbmp` 是叠加层,用子 `Sprite`。
|
||||
|
||||
### 5.2 `ifast_mydrawtext(spid, recid, sp_x, sp_y, sp_w, sp_h)` — 在精灵上绘制文字
|
||||
|
||||
**旧语义**(`:2643`):在精灵 `spid` 上画 `gameabc_GameTxt.GameTxtList[recid]` 的 `Text`(颜色 `Color`,字号 `sp_h`,`textBaseline=top`)。`spid<=0` 时画在画布 `(sp_x, sp_y)`。
|
||||
|
||||
**Cocos 对应**:`cc.Label`。若文字要「贴」在某个精灵上,就挂一个子 `Label` 节点(与 §5.1 同理);若文字就是精灵自己的 caption,直接 `set_self(spid, 7, text)` → `label.string`(§3 cpid=7)。
|
||||
|
||||
> 注意:`recid` 在这里是 **`GameTxtList` 的索引(文字 id)**,不是图片资源号——与 `ifast_mydrawbmp` 的 `recid` 是两套 id。`set_rec`/`get_rec`(`:3089`/`:3092`)读写的就是这份文字列表(§5.6)。
|
||||
|
||||
### 5.3 `ifast_mydrawsprite(spid, spidsourse, sp_x, sp_y)` — 在精灵上绘制另一个精灵
|
||||
|
||||
**旧语义**(`:2596`):把精灵 `spidsourse` 画到 `spid` 的偏移 `(sp_x, sp_y)` 处(临时改 `spidsourse` 坐标、`draw`、恢复)。
|
||||
|
||||
**Cocos 对应**:直接把 `spidsourse` 作为 `spid` 的**子节点**,设 `position = (sp_x, sp_y)`。保留式场景图下「一个节点画在另一个节点内」就是**父子关系**,无需每次临时改坐标再恢复。
|
||||
|
||||
### 5.4 数字图片精灵
|
||||
|
||||
旧引擎**没有专门的数字 API**,数字只有两条路,Cocos 各对应:
|
||||
|
||||
| 旧路径 | 旧实现 | Cocos 对应 |
|
||||
|---|---|---|
|
||||
| **文字数字** | `set_self(spid, 7, "积分:" + score)`(caption)→ 引擎 `fillText` | `Label.string`(系统字体 / TTF)——**平台层绝大多数数字走这条,1:1 平替** |
|
||||
| **图片数字** | `ifast_mydrawbmp` 逐位切数字图集的帧、手算 `bmp_x = 位×字宽` | **`Label` + BMFont(`.fnt`)**——原生数字图片精灵:一个节点、自动逐位取字模、自动对齐/字距/缩放,**彻底取代手动画子图 + 手算偏移** |
|
||||
|
||||
**BMFont 落地的注意点**:旧数字图集是「多帧网格 PNG」(`w × h = frame_all`,A §0.3),**不是 BMFont 格式**。要用 BMFont,须把 0–9(及可能的小数点/`+`/`-`/`:`)字模重打包成 `.fnt`+`.png`。若美术暂不重打包,可降级用「逐位 `Sprite` + 数字图集帧」手动排——**这是把旧引擎手工做法搬过来,不推荐**,只作为过渡。
|
||||
|
||||
### 5.5 `ifast_addtospritefromspritecopy(fspid, copyspid, x, y, tag)` / `ifast_dllpritefromspritecopy(fspid, tag)`
|
||||
|
||||
**旧语义**(`:3556`,核心 `GameObject.addtospritefromspritecopy` `:1624`):把模板精灵 `copyspid` **深拷贝**(`copyme` 复制全部字段、共享 `image`/`uidata`)出一份副本,挂到父精灵 `fspid` 下、放到 `(x, y)`、打 `tag`、入 `addlist`,返回 id 串 `fspid+'add'+tag`。配套 `ifast_dllpritefromspritecopy`(`:3564`)按 `tag` 删除副本。
|
||||
|
||||
典型用法是**列表渲染**:一个模板 + 循环 N 份,`y = 首项 + spaceY*i`(房间列表 `11_GameUI.js:3707`、任务列表 `:4679`、VIP 榜 `:9629`)。
|
||||
|
||||
**Cocos 对应**(三层,从平替到更优):
|
||||
|
||||
| 层级 | Cocos 做法 | 说明 |
|
||||
|---|---|---|
|
||||
| 平替 | `cc.instantiate(prefab)` + `node.setParent(parent)` + `node.setPosition(x, y)`;删除 `node.destroy()` / `node.removeFromParent()` | 模板做成 **prefab**,等价深拷贝 |
|
||||
| 更优 | `cc.Layout`(垂直/水平/网格) | 自动排布,**干掉 `spaceY*i` 手算** |
|
||||
| 更优 | `cc.ScrollView` + `NodePool`(复用,不反复 new/destroy) | 长列表滚动 + 对象池回收 |
|
||||
|
||||
```ts
|
||||
// 平替(等价深拷贝 + 定位)
|
||||
const item = instantiate(templatePrefab);
|
||||
item.setParent(listParent);
|
||||
item.setPosition(x, y);
|
||||
|
||||
// 更优:对象池复用(长列表)
|
||||
const item = pool.get() ?? instantiate(templatePrefab);
|
||||
item.setParent(listParent);
|
||||
item.setPosition(x, y); // 交给 Layout 则连这行都省
|
||||
// 用完:pool.put(item)
|
||||
```
|
||||
|
||||
`ifast_dllpritefromspritecopy(fspid, tag)` → `node.destroy()`;要复用 → `NodePool.put(node)` / `NodePool.get()`。
|
||||
|
||||
> 与 A 的衔接:A §0.2 指出「991 个对象里一部分是模板、一部分是容器」,且 A **有意不识别模板/容器**(A §1.5)。识别「哪个 ObjectID 是 `ConstVal.myRoomList.bgSp` 这类模板」是 **C(平台逻辑移植)** 的职责;B 只负责给出「克隆模板 → instantiate/Layout/Pool」这条映射规则。
|
||||
|
||||
### 5.6 `set_rec(stringid, stringv)` / `get_rec(stringid)` — 读写文字表
|
||||
|
||||
**旧语义**(`:3089`/`:3092`):读写 `gameabc_GameTxt.GameTxtList[stringid].Text`——一份**全局文字串表**(资源号是文字 id,非图片)。
|
||||
|
||||
**Cocos 对应**:`get_rec(id)` 读取的文本最终进某个 `Label`。文本常量本身在 C 里应由 `theme.fonts` / 文案表提供(见 `2026-08-27-ui-asset-and-skin-design.md` §4),不散落各处。**不要**为 `GameTxtList` 单独建一份全局可变文字表——它是旧引擎「无富文本能力」的产物,Cocos 用 `Label` 直接承载。
|
||||
|
||||
---
|
||||
|
||||
## 6. 落地约定(C 移植时遵守)
|
||||
|
||||
1. **`spid` = `ObjectID` = `legacy.objectId`**(A §1.3),三者同源;C 侧通过 A 的 `manifest.json`(ObjectID → 节点路径)由 `spid` 定位节点。
|
||||
2. **换帧统一走 `FrameSet`**(A §3.4):`set_self(spid, 43, n)` → `frameSet.setFrame(n)`,帧号同为 1 基,无需心算减一。
|
||||
3. **坐标/尺寸**一律经 A §2.2 的换算后再赋值;本规范不重复坐标公式。
|
||||
4. **叠加绘制(§5.1/5.2)永远用子节点**,不得试图把多张图塞进一个 `Sprite`。
|
||||
5. **列表渲染(§5.5)默认上 `Layout` + `NodePool`**;只有一次性、数量少、非滚动的场景才用裸 `instantiate`。
|
||||
6. 遇到 ❓ 属性号或本表未列的 API,**回到 `gameabc.min.js` 对应 `switch`/函数补齐**,再回填本表——**禁止臆测**(第二准则)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 红线与待验证
|
||||
|
||||
**红线**
|
||||
- 不手写 `.prefab`/`.scene`/`.meta`(CLAUDE.md)——本规范的落地产物仍走 MCP / 编辑器。
|
||||
- 不臆测属性号语义;未定项以 ❓ 标记,C 移植时按需逆向。
|
||||
- 帧序/切帧/`FrameSet`/图集命名 只以 A §0.3/§3 为权威,本文不复写。
|
||||
|
||||
**已解决(本次逆向坐实,2026-08-30)**
|
||||
- 属性号 34/35/36/45/46/51/57 语义已逆向完成并回填 §3(由 `gameabc_charge` / `gameabc_toclient` / `drawone` 源码坐实)。`ap`=透明度(0–255 → `UIOpacity.opacity`)**实证成立**,不再是推断。
|
||||
- `mode` 相对运算占比已统计:全平台层仅 `mode=1`(加法)约 27 处(其中 6 处 caption 的 mode 无效),`mode=2/3/4` 零使用 → **C 侧无需相对运算辅助方法**(详见 §3)。
|
||||
|
||||
**待验证(落地前)**
|
||||
|
||||
| # | 待验证 | 方法 |
|
||||
|---|---|---|
|
||||
| 1 | 图片数字是否真用 BMFont(需美术重打包字模),还是暂用逐位 Sprite 过渡 | 看子游戏层数字素材现状,与美术确认 |
|
||||
| 2 | `ifast_mydrawbmp` 的「叠加 vs 换帧」判定在 C 侧是否要更细(有些 spid 自身无图但语义上是「画内容」而非「换帧」) | C 移植首屏时抽样核对 |
|
||||
@@ -0,0 +1,276 @@
|
||||
# 旧 gameabc 平台逻辑 → Cocos TypeScript 移植(子系统 C·平台逻辑移植)
|
||||
|
||||
> 状态:已逆向、待与用户确认。
|
||||
> 适用工程:`cocoscreator_projects/YouleNexus`(框架真源)。
|
||||
> 数据源:`projects/Game_Surface_3/js/00_Surface/`(12 文件)、`js/gamemain.js`、`js/gameabc.min.js`(均逐行逆向,行号见各节引用)。
|
||||
> 关联:本文是 A §0.1 定义的**第三个子系统**,依赖 **A(`2026-08-28-legacy-ui-migration-design.md`)** 与 **B(`2026-08-30-legacy-engine-api-mapping.md`)**;目标分层落到 **框架 spec(`2026-06-28-cocos-framework-design.md`)** 的 §3/§4。
|
||||
> 范围:旧工程的**逻辑流程与逻辑架构**——四层结构、数据流向、状态归属、核心流程、平台层对引擎 API 的依赖清单——到 Cocos 的移植。**不含**单条 API 的属性号语义(B §3)与渲染等价物(B §5),本文只引用。
|
||||
|
||||
---
|
||||
|
||||
## 0. 最高原则
|
||||
|
||||
承接 A/B 的两条原则(**原理等价**、**表现等效原理自由**),本文新增第三条,专门约束「逻辑流程一致性」:
|
||||
|
||||
> **原则三 · 复刻业务逻辑,不复刻引擎机制**:旧引擎的主循环、双缓冲、帧内事件派发、层叠渲染、对象注册表等,都是「无 Cocos 时手工搭的引擎」——Cocos 引擎**自带等价物**,一律交给 Cocos 运行时,**不得在 TypeScript 里重写一遍渲染循环 / 命中检测 / 定时器调度**。要复刻的是**业务逻辑流程**:启动门控、登录/进房/重连状态机、收发包路由、状态归属边界、界面切换时机——这些是协议契约与业务正确性的承重墙,必须逐条对齐。
|
||||
|
||||
判定「该复刻还是该交给引擎」的判据:**看它属不属于协议契约 / 业务状态机**。是 → 复刻;只是旧引擎的实现手段 → 交给 Cocos。
|
||||
|
||||
---
|
||||
|
||||
## 1. 四层架构 → Cocos 框架分层
|
||||
|
||||
旧前端是**四层**(自底向上);目标框架 spec §3 已定义 **6 层**(`core/net/protocol/platform/ui/sdk`)。两者不是 1:1,映射如下:
|
||||
|
||||
| 旧层 | 旧文件 | 在 Cocos 的落点 | 说明 |
|
||||
|---|---|---|---|
|
||||
| **引擎层** | `js/gameabc.min.js` + Spine + Canvas | **无对应**,由 Cocos 引擎接管 | 主循环/渲染/命中/定时器/资源/对象注册全是引擎机制(§2) |
|
||||
| **桥接层** | `js/gamemain.js`(306 行) | `core/EventBus` + `sdk/` 回调钩子分发 | `gameabc_face` 全局回调表 → 事件总线;三方扇出 → `IGameModule` 钩子(§1.1) |
|
||||
| **平台层** | `js/00_Surface/*`(12 文件) | `net` + `protocol` + `platform` + `ui` 四层 | 见 §3 逐文件映射 |
|
||||
| **子游戏层** | `js/01_SubGame/*`(3 文件) | `sdk/`(子游戏实现 `IGameModule`) | 对局态 + `serialize()/restore(deskinfo)`(框架 spec §3 零耦合规则 3) |
|
||||
|
||||
> 关键澄清:旧「桥接层」`gamemain.js` **没有业务逻辑**,只是「把引擎回调扇出到三个命名空间」的接线盒。Cocos 里它**消失**了——引擎回调变成 Cocos 事件,扇出变成 `IGameModule` 的一组具名钩子(`onPlayerJoin`/`onReady`/`onDissolve`/`onOffline`…)。不要为它单独建一个「gamemain 模块」。
|
||||
|
||||
### 1.1 桥接层 `gameabc_face` → 事件分发
|
||||
|
||||
旧 `gamemain.js` 的机制是「引擎反射调用 `gameabc_face.<事件名>` 具名函数,函数内部按固定三方扇出」。Cocos 对应为**两种机制**,各管一半:
|
||||
|
||||
| 旧引擎回调(`gameabc_face.*`) | 旧扇出目标 | Cocos 对应 |
|
||||
|---|---|---|
|
||||
| `gamestart` | 仅 `Logic.AppStart()` | 引擎 `onLoad`/启动时序 → `platform` 启动编排 |
|
||||
| `onloadurl`(资源加载进度) | `GameUI.onloadurl` | Cocos 资源系统 `Resources.load` 的 `onProgress`,**无需逐张计数** |
|
||||
| `ontimer` / `ontimer_<objid>`(每精灵定时器) | 仅 `GameUI.utlontimer` | `component.schedule`(B §3 cpid=57 已映射) |
|
||||
| `mousedown`/`mousedown_nomove`/`mouseup`/`mousemove` | `GameUI` + `Game_Modify` + `gameCombat` 三方 | Cocos 节点 `Button`/`EventTouch` 事件 + `IGameModule` 钩子 |
|
||||
| `ani_doend`/`box_doend`(动画结束) | `GameUI` + `gameCombat` | `Animation`/`Tween` 完成回调 |
|
||||
| `gamemydraw`/`gamemydrawbegin`/`gamebegindraw`/`gameenddraw` | 三方 | **无对应**(渲染循环交给引擎,业务「帧同步」需求另议) |
|
||||
| `onresize` / `chongzhi` / `tcp*` / `httpmessage` | 空桩 | 不迁移(空实现) |
|
||||
|
||||
> 「初始化 / 定时器」旧只走平台层;「触屏 / 动画 / 绘制」旧三方扇出。Cocos 里前者是 `platform` 内部事务,后者是 `IGameModule` 的具名钩子(子游戏按 spid 过滤是否处理)——这一「按 spid 过滤」的模式在 Cocos 里**不再需要**,因为钩子是类型化的、子游戏只收自己关心的。
|
||||
|
||||
---
|
||||
|
||||
## 2. 引擎运行时机制:引擎自带 vs 业务复刻
|
||||
|
||||
任务逆向出的引擎运行时(33fps 主循环、backBuffer 双缓冲、帧内事件派发、层管理、`sp+id` 对象注册表、100 并发资源加载),逐条判定归属:
|
||||
|
||||
| 旧引擎机制 | 源码位置 | 判定 | Cocos 等价物 |
|
||||
|---|---|---|---|
|
||||
| 33fps `requestAnimationFrame` 主循环(`FPS=33` 硬编码,忽略数据 `fps=30`) | `tick()` `:1954`、`checkrun()` `:1858` | **引擎自带,不复刻** | Cocos 渲染循环(`director`) |
|
||||
| backBuffer 双缓冲 + 脏标记 blit | `drawone()` `:1874`、`draw()` | **引擎自带,不复刻** | Cocos 场景图局部重绘 |
|
||||
| 帧内事件派发(DOM 只记 `g_mouse`,回调在 `draw()` 里派发) | `gameabc_check_click` `:4095`、`check_click1` `:4068` | **引擎自带,不复刻** | Cocos 事件系统(`EventTouch`) |
|
||||
| 每精灵定时器 `ontime`/`click_time` | `drawone` `:1878-1900` | 引擎自带 | `component.schedule`(B §3 cpid=57) |
|
||||
| 层管理 `LayerList`/`showorder`/`set_level`/`set_level_up` | `:3102`/`:3114` | **引擎自带,且平台层从未直接调用**(§6.5) | Cocos 节点树 + `node.active` |
|
||||
| `sp+id` 对象注册表 / `levelobj` 反向引用 | — | 引擎自带 | Cocos 节点树 + `manifest.json`(A §1.3) |
|
||||
| 100 张并发 + 10ms 节流资源加载 | `load_img1` `:4454`/`load_img2` `:4597` | 引擎自带 | Cocos `Resources`/`Asset Bundle` |
|
||||
|
||||
**结论**:上表**没有一条需要复刻**。引擎层的价值在「协议无关的渲染/输入/资源」,Cocos 引擎整体接管;真正要搬到 TypeScript 的是 §3–§6 的平台层业务。
|
||||
|
||||
---
|
||||
|
||||
## 3. 平台层模块 → framework 分层落点
|
||||
|
||||
平台层 12 个文件,逐个落到框架 6 层(框架 spec §3 的映射表已给粗对应,本文精确到「文件 → 模块」):
|
||||
|
||||
| 旧文件(行数) | 职责 | framework 层 | 落点模块(示意) |
|
||||
|---|---|---|---|
|
||||
| `02_Const.js`(822) | 协议常量 + 静态配置 | `protocol` + `core` | `protocol/routes.ts`、`protocol/rpc.ts`(唯一来源);UI 常量 → `ui/theme` |
|
||||
| `00_minhttp.js`(656) | WebSocket/XHR 封装 + 工具 | `net` + `core` | `net/NetClient` + `core/utils`(`min_*` 40+ 工具) |
|
||||
| `09_Net.js`(897) | 收发信封 + 路由表 | `protocol` + `net` | `protocol/platform-rpc.ts`(`send_*`/`handle_*`) |
|
||||
| `04_Data.js`(584) | 全局态 `GameData` | `platform` | `platform/stores/app-store.ts`(`AppStore`) |
|
||||
| `06_Player.js`(575) | 玩家结构 `Player`/`C_Player` | `platform` | `platform/stores/player-store.ts`(`PlayerStore`) |
|
||||
| `07_Desk.js`(1427) | 房间状态机 `Desk` | `platform` | `platform/stores/room-store.ts`(`RoomStore`) |
|
||||
| `12_Logic.js`(2444) | 连接/重连/分发编排 | `platform` + `net` | `platform/session.ts`(登录/重连编排)+ `net/NetClient` |
|
||||
| `11_GameUI.js`(9862) | 平台全部 UI | `ui` | `ui/` 下 Prefab + 各界面控制器 |
|
||||
| `08_Utl_Output.js`(1034) | 平台对子游戏的输出/工具 API | `sdk`(facade) | `sdk/GameContext`(`getMyInfo`/`getRoomcode`/`getPlayerList`…) |
|
||||
| `05_Func.js`(4185) | 原生/系统能力封装 | `platform` + `sdk` | 原生桥接归 `sdk`;纯工具归 `core/utils` |
|
||||
| `03_Banwords.js` | 违禁词 | `core` | `core/utils` |
|
||||
| `10_Game.js` | (零散 `set_self/get_self`) | `ui` | 并入对应界面控制器 |
|
||||
|
||||
### 3.1 三个状态机的职责与唯一归属(框架 spec §3 零耦合规则 3 的细化)
|
||||
|
||||
| 状态机 | 旧归属 | framework Store | 唯一职责 |
|
||||
|---|---|---|---|
|
||||
| `GameData` | 连接/配置/资产/面板态 | `AppStore` | `Server/AjaxUrl/AgentId/ChannelId/GameId`、`hallConfig/sysConfig`、各类列表 |
|
||||
| `Desk` | 房间/座位/局状态 | `RoomStore` | `PlayerList[]`、`roomcode/stage/state/roomtype/warcnt`、解散投票 `AgreeList` |
|
||||
| `C_Player` | 自己的状态 | `PlayerStore` | `playerid/bean/roomcard/seat/status/isprepare/state` |
|
||||
|
||||
> 旧 `C_Player` 与 `Desk.PlayerList[C_Player.seat]` 是**同一玩家的两份镜像**,靠 `Player.SetDeskInfo/getDeskInfo` 手工同步(`06_Player.js:177-180` 改 bean 时显式回写 `Desk.PlayerList[seat].bean`)。Cocos 里这是**单一来源原则的反例**:`PlayerStore` 应持有「座位 → 玩家」的**唯一**响应式映射,自己的座位只是其中一个下标;消除双份镜像的手工同步。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据流向与调用方向(谁调谁、谁改谁)
|
||||
|
||||
```
|
||||
旧(四层 + 双向手工刷新) Cocos(单向依赖 + 响应式)
|
||||
────────────────────────────── ──────────────────────────────
|
||||
引擎回调 ─► gamemain 扇出 ─► GameUI/Logic/子游戏 事件/钩子 ─► 单一 handler
|
||||
收包 onmessage ─► Net.<rpc> 薄转发 ─► Desk/C_Player 收包 ─► Router ─► Store 更新 ─► UI 响应式刷新
|
||||
└► 改状态后 手工调 GameUI.* 刷界面 └► (无手工刷 UI,Store 变化自动驱动)
|
||||
```
|
||||
|
||||
**关键差异(移植时必须理解的)**:
|
||||
- 旧是「改状态 → 手工调 `GameUI.xxx()` 刷新」。Cocos 是「Store 响应式 → UI 自动刷新」,**消灭 `Desk`/`C_Player` 方法里那批 `GameUI.setHallRoomCard`/`setHallStar` 手工回调**。
|
||||
- 旧 `Net.<rpc>` 收包函数几乎全是「薄转发 + 菊花」(`Desk.<rpc>(_msg)` 或 `C_Player.<xxx>` 或 `gameCombat.<xxx>`,夹 `GameUI.StartLoad()/EndLoad()`)。Cocos 里「薄转发」退化为 `protocol` 层的 typed handler → 直接调 Store 方法,「菊花」变成全局 loading 态。
|
||||
|
||||
### 4.1 收包路由分界(协议契约,必须逐字对齐)
|
||||
|
||||
`12_Logic.js:258-264` 的分发是**平台/子游戏的分水岭**:
|
||||
|
||||
```
|
||||
route ∈ { platform, agent, room } ─► 平台层 Net.<rpc>(protocol/platform-rpc)
|
||||
route = <game route> ─► 子游戏 Game_Modify._ReceiveData(sdk → IGameModule.onReceive)
|
||||
```
|
||||
|
||||
框架 spec §4 已把这条分界固化为 `Router`,本文只强调:**这个三分支的 route 判定是协议红线**,`platform/agent/room` 之外的任何 route 都不得被平台层拦截或解析。
|
||||
|
||||
### 4.2 发包信封(协议契约,逐字节对齐)
|
||||
|
||||
`Net._SendData`(`09_Net.js:12`)组装 `{app:"youle", route, rpc, data}`;`netType==0` 走 WebSocket,`netType==1` 走 `Func.AjaxHttp2`(HTTP 兜底,`player_login` 特判 `putMsg="playerLogin"`)。Cocos 侧 `net/NetClient` + `protocol` 编码必须保持信封结构与两条传输路径(含 HTTP 兜底),见框架 spec §4 与 `native-bridge-contract` 技能。
|
||||
|
||||
---
|
||||
|
||||
## 5. 状态归属与边界
|
||||
|
||||
| 归属 | 状态 | 旧位置 | Cocos 位置 |
|
||||
|---|---|---|---|
|
||||
| 平台-连接/配置/资产 | `Server/AgentId/ChannelId/GameId`、`hallConfig/sysConfig`、列表 | `GameData` | `AppStore` |
|
||||
| 平台-房间 | `PlayerList[]`、`roomcode/stage/state`、`AgreeList`、`roomtype` | `Desk` | `RoomStore` |
|
||||
| 平台-自己 | `playerid/bean/roomcard/seat/status/isprepare` | `C_Player` | `PlayerStore` |
|
||||
| 平台-UI 瞬态 | `onstate[]/isprepare[]/VoteList/Mem`(方位映射后显示态) | `GameUI` 顶部变量 | `ui` 各控制器局部态 |
|
||||
| 平台-本地持久化 | 开关/roomlist/playerid/machineId 等 | `Utl` storage key | `core/storage`(key 按 `GameId+AgentId` 拼,保持兼容) |
|
||||
| **子游戏-对局态** | 手牌/出牌历史/轮次/分数/结算 | `Game_Modify`/`gameCombat` | 子游戏自身命名空间 + `serialize()/restore(deskinfo)` |
|
||||
|
||||
**边界判据(三条,移植时逐条守住)**:
|
||||
1. **路由判据**:`route ∈ {platform,agent,room}` 归平台层,其余归子游戏(§4.1)。
|
||||
2. **对局态透传不解析**:服务器在 `player_login` 回包下发 `deskinfo` 对局快照,`Desk.login` **原样**交给 `Game_Modify.Reconnect(deskinfo)`(`07_Desk.js:419-426`),平台层**不持有、不解析**对局态。Cocos 里是 `IGameModule.onReconnect(deskinfo)`,`RoomStore` 只透传快照、不拆解。
|
||||
3. **同一玩家单一来源**:消除 `C_Player` 与 `Desk.PlayerList[seat]` 双镜像(§3.1)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 核心流程(逻辑流程一致性基准)
|
||||
|
||||
以下五条流程是「逻辑流程一致」的验证基准,Cocos 侧必须**按同一次序、同一门控条件**复刻(实现手段可不同,流程与时序必须一致)。
|
||||
|
||||
### 6.1 启动流程(页面加载 → 首屏登录)
|
||||
|
||||
旧:资源加载(`onloadurl` 逐张计数到 100)→ `gamestart` → `Logic.AppStart`(定 netType/isGameHall、`registerFunction`、`setChannelId→setGameServer→setAgentId→getVersionState`、`new C_Player`)→ `get_config`(HTTP 取 `player_server_tcp/game_server_tcp/urlserver`)→ `getConfig_Succ`(写 `GameData.Server/AgentId`、`GameInit`)→ `firstConnect` 建 WS。
|
||||
|
||||
**首屏门控四条件**(全齐才进登录页,`GameUI.JumpWxAuth()`):
|
||||
1. `LoadCount==100`(资源)
|
||||
2. `firstConnect_Succ`(WS onopen)
|
||||
3. `getJSONState`(配置就绪)
|
||||
4. `Timesup`(加载计时器 `Game_Config.Max.showtime` 到点)
|
||||
|
||||
> 门控四条件的**时序语义必须保留**(缺一不进登录页),但实现手段升级:资源计数交给 Cocos 加载系统,计时器用 `scheduleOnce`,四条件合成一个「就绪信号」而非各自散落置位。
|
||||
|
||||
### 6.2 登录 / 进大厅 / 进房间
|
||||
|
||||
`Net.Send_login`(组包 agentid/openid/nickname/…/machineid)→ `Net.player_login` → `Desk.login`,按 `roomcode` 分支:
|
||||
- **无 `roomcode`** → `Game_Modify.closeGameScene()` + `GameUI.JumpMenuScene()`(大厅)+ 按 `Logic.JudgeShow()` 弹公告。
|
||||
- **有 `roomcode`** → `Logic.updateMainSceneData(roomtype)`(=`Game_Modify.onCreateDesk` + `Desk.Create`)→ `GameUI.MainScene(true)` → 按 `deskinfo` 有无走 `Game_Modify.Reconnect(deskinfo)` 或 `ReconnectNoMakewar()`。
|
||||
|
||||
### 6.3 断线重连(`isbattle==1` → `Reconnect(deskinfo)`)
|
||||
|
||||
- **断线检测**:`game_websocket.onclose` → 置 `netWorkSate=false`;非首次连接 `NetType=1` + `TryConnect()` + `OpenDisConnect()`。
|
||||
- **重连后**:`onopen` → 若 `isLogin` 重发 `Send_login`。
|
||||
- **触发点**(`07_Desk.js:419-426`):登录回包带 `deskinfo` 时 → `Game_Modify.Reconnect(deskinfo)`;否则 `ReconnectNoMakewar()`。子游戏必须能**反序列化同一结构**(与 `docs/protocol/` 的 `deskinfo` 快照对齐,B 已强调)。
|
||||
- **旁路**:`self_join_room`(`07_Desk.js:649`)带 `deskinfo` → `Desk.stage=1` + `Game_Modify.DeskInfo(deskinfo)`(他人未开战牌桌数据)。
|
||||
|
||||
### 6.4 对局流程(`Desk` 状态机 + 子游戏协作)
|
||||
|
||||
开战 / 准备 / 对局收发包 / 解散投票 / 离桌 / 结算投降,均以 `Desk` 状态机为骨架、`Game_Modify` 为协作方:
|
||||
|
||||
| 流程 | 平台层(`Desk`/`C_Player`) | 子游戏(`Game_Modify`,Cocos `IGameModule`) |
|
||||
|---|---|---|
|
||||
| 开战 | `self/other_makewar` → `stage=1`、`ChangeExit(0)`、`HideStartScene()` | `StartWar(_msg)` 建对局 |
|
||||
| 准备 | `player_prepare` → 改座位 `isprepare` + `SetIsprepare` | `onReady` |
|
||||
| 对局中 | route 非 platform/agent/room 的包**完全不介入** | `_ReceiveData`(`onReceive(rpc,data)`) |
|
||||
| 解散投票 | `state=1` + `AgreeList` 累积 + `OpenApply` | `Free(deskfree)`(`free_room` 时) |
|
||||
| 离桌 | `stage==1` → `breakRoom()`;其余 `playerLeaveRoom` 等 | `breakRoom/playerLeaveRoom/playerOffline/changeSeat` |
|
||||
| 结算/投降 | `get_player_grade1/2`→`gameCombat`;`beanroom_surrender` | `onSurrender` |
|
||||
|
||||
### 6.5 界面切换(**不是 `set_level`**)
|
||||
|
||||
> 重要澄清(全仓 grep 实证):`set_level`/`set_level_up` 是**引擎层函数**,平台层**从未直接调用**。平台层的「界面切换」机制是**精灵级显隐 + 场景哨兵**:
|
||||
|
||||
- `set_self(spid, 37, vsb)`(cpid=37 可见性)+ `set_group(groupid, 37, vsb)` 整组显隐。
|
||||
- 界面骨架:`GameUI.invisible()`(全隐藏)→ 各界面只点亮自己的 group(登录 `set_group(2,37,1)`、大厅 `set_group(3,37,1)`、房间 `set_group(21,37,1)`、各弹窗各自 group)。
|
||||
- **场景哨兵**:`get_self(149,37)` = 「是否在房间主界面」、`get_self(4,37)` = 「大厅」标志,被 `Utl.isMainScene`/`Logic.checkRoom` 读。
|
||||
|
||||
Cocos 等价:`set_self(spid,37,vsb)` → `node.active`(B §3 cpid=37);「界面」→ 独立的**场景 / 预制根节点**;「场景哨兵」→ `AppStore` 里显式的 `currentScene` 状态(而不是反查某个精灵的可见性)。**不要**去找 `set_level` 的对应物。
|
||||
|
||||
---
|
||||
|
||||
## 7. 平台层 gameabc API 依赖清单(19 个,分域)
|
||||
|
||||
全平台层 `00_Surface` 实际调用 **19 个引擎 API,共 2305 处**,其中 `set_self`/`get_self`/`set_group` 三项占 88.6%(2042 处),且集中在 `11_GameUI.js`。按域分列;**B 已覆盖的引用 B,本文只补 B 未覆盖的映射方向**。
|
||||
|
||||
| 域 | API(次数) | Cocos 对应方向 |
|
||||
|---|---|---|
|
||||
| 属性读写 | `set_self`(1525)、`get_self`(373)、`set_group`(144) | 属性号对照见 **B §3**;`set_group(groupid,37,vsb)` 整组显隐 → 该 group 父节点的 `active`(A §2.1 已并入父节点) |
|
||||
| 裁剪/记录 | `set_clip`(21) | 设置裁剪区域(滚动列表/进度条)→ `cc.Mask` / `ScrollView` 视口裁剪 |
|
||||
| 记录 | `set_rec`(4)、`get_rec`(2) | 见 **B §5.6**(文字表 → `Label`,不建全局可变文字表) |
|
||||
| 窗口变量 | `set_windows`(1) | ⚠️ 待逆向(`12_Logic.js:523 set_windows(2,"var","1977")`,仅 1 处,落地前看用途) |
|
||||
| 渲染绘制 | `ifast_mydrawbmp`(19)、`ifast_mydrawtext`(2) | 见 **B §5.1/§5.2** |
|
||||
| 精灵绘制 | `ifast_mydrawsprite`(—) | 见 **B §5.3** |
|
||||
| 批量加载 | `ifast_loadsprite`(1) | 批量预加载精灵 → Cocos `Resources.loadDir` / 图集预载(引擎自动,通常无需) |
|
||||
| 实例/资源 | `ifast_addtospritefromspritecopy`(77)、`ifast_dllpritefromspritecopy`(69) | 见 **B §5.5**(列表 → `Layout`/`ScrollView`/`NodePool`) |
|
||||
| 命中/检测 | `ifast_check_add`(12)、`ifast_checkimg`(1) | 按坐标检测精灵 → Cocos 节点事件系统天然覆盖;`checkimg` 图片已加载检查 → 资源系统 |
|
||||
| 对象查找 | `ifast_getobj`(4) | 按 id 取引擎对象 → `node.getComponent` / `manifest.json`(A §1.3)定位 |
|
||||
| 工具/数学 | `ifast_random`(14)、`ifast_abs`(14) | 纯 JS:`Math.random()`/`Math.abs()`,**无引擎依赖** |
|
||||
| 引擎单例 | `gameabc_face`(14)、`gameabc_Voice`(8) | `gameabc_face`(取 div/canvas ctx)→ 场景根/Canvas 持有;`gameabc_Voice`(语音列表)→ 音频资源清单 + `AudioSource` |
|
||||
|
||||
**网络域(独立,见 `native-bridge-contract` 技能)**:平台层网络**不走**引擎的 `ifast_ws/tcp/ajax/http`(0 次调用),而是自有 `00_minhttp.js` 的 `min_tcp`(原生 `new WebSocket`)+ `min_http`(原生 `XMLHttpRequest`),收发经 `Net.ws_tcp.send(JSON.stringify(msg))`。Cocos 用 `WebSocket` 封装 + 协议信封逐字节对齐。
|
||||
|
||||
**任务提及但平台层实际未使用(避免白做迁移)**:
|
||||
|
||||
| 未使用 API | 实测 |
|
||||
|---|---|
|
||||
| `ifast_ws`/`ifast_tcp_*`/`ifast_ajax`/`ifast_http`/`ifast_split` | 0 次(平台层改用自有 `min_tcp`/`min_http`) |
|
||||
| `get_selfdiv`/`set_selfdiv` | 0 次 |
|
||||
| `set_level`/`set_level_up`/`openurl`/`showmessage` | 0 次 |
|
||||
| `gameabc_Object`/`gameabc_Image`/`gameabc_Layer`/`gameabc_GroupList`/`gameabc_GameTxt` | 0 次(仅 `gameabc_face`、`gameabc_Voice` 被用) |
|
||||
| `up_imgurl` | 平台层自有 `Func.up_imgurl`(`05_Func.js:1710`),非引擎 API |
|
||||
|
||||
---
|
||||
|
||||
## 8. 逻辑流程一致性检查点(验证基准)
|
||||
|
||||
迁移完成后,以下检查点逐一核验(作为「逻辑流程一致」的验收单):
|
||||
|
||||
- [ ] **信封**:`{app:"youle", route, rpc, data}` 结构与 `docs/protocol/` 逐字节对齐;`route` 三值 `platform/agent/room` 判定正确。
|
||||
- [ ] **收包分界**:`route ∈ {platform,agent,room}` → 平台层;其余 → `IGameModule.onReceive`;平台层不拦对局包。
|
||||
- [ ] **发包路径**:`netType==0` WebSocket / `netType==1` HTTP 兜底两条都在;`player_login` 的 `putMsg="playerLogin"` 特判保留。
|
||||
- [ ] **启动门控四条件**:资源/WS onopen/配置/加载计时器,缺一不进登录页。
|
||||
- [ ] **登录分支**:`roomcode` 有无 → 大厅 or 房间;`deskinfo` 有无 → `onReconnect` or `ReconnectNoMakewar`。
|
||||
- [ ] **断线重连**:onclose → 重连 → 重发登录 → `deskinfo` 反序列化恢复对局(子游戏 `restore`)。
|
||||
- [ ] **状态归属**:`RoomStore`/`PlayerStore`/`AppStore` 三态机职责唯一;`C_Player` 与 `PlayerList[seat]` 无双镜像。
|
||||
- [ ] **对局态透传**:平台层不解析 `deskinfo`,原样交 `IGameModule`。
|
||||
- [ ] **界面切换**:用 `node.active` + `AppStore.currentScene`,不搬 `set_level`。
|
||||
- [ ] **原生接口**:`window.settings` 同步取值 + WVJB 异步桥,接口名/数据格式/URL 构造与旧逐字一致(见 `native-bridge-contract`)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 落地约定 / 红线 / 待验证
|
||||
|
||||
### 9.1 落地约定
|
||||
|
||||
1. **模块名遵循框架 spec §3**:旧文件 → framework 层的映射(§3 表)是唯一落点,不得另起命名。
|
||||
2. **Store 响应式驱动 UI**:`RoomStore`/`PlayerStore`/`AppStore` 状态变 → UI 自动刷新,**消灭** `Desk`/`C_Player` 里那批 `GameUI.*` 手工回调。
|
||||
3. **路由/信封/deskinfo 只认 `docs/protocol/`**:任何网络层疑问先读协议文档,不臆测(CLAUDE.md 第一准则)。
|
||||
4. **遇到 ❓/⚠️ API**(如 `set_windows`)回到 `gameabc.min.js` 对应处补齐再回填,**禁止臆测**(第二准则)。
|
||||
|
||||
### 9.2 红线
|
||||
|
||||
- 不手写 `.scene`/`.prefab`/`.meta`(CLAUDE.md)——本规范落地产物仍走 MCP。
|
||||
- 不复刻引擎机制(主循环/双缓冲/帧内命中/对象注册表)——交给 Cocos 运行时。
|
||||
- 不改变协议信封、route 判定、`deskinfo` 透传边界(第一准则)。
|
||||
|
||||
### 9.3 待验证(落地前)
|
||||
|
||||
| # | 待验证 | 方法 |
|
||||
|---|---|---|
|
||||
| 1 | `set_windows(2,"var","1977")` 的语义(仅 1 处,`12_Logic.js:523`) | 看该调用上下文,判定是否需迁移或可直接丢弃 |
|
||||
| 2 | `Utl` storage key(`GameId+AgentId` 拼接)在新工程是否要保持原 key 以兼容旧缓存 | 与后端/运营确认是否需沿用旧 key |
|
||||
| 3 | 首屏门控四条件里「加载计时器 `showtime`」的精确时长与超时行为 | 读 `Game_Config.Max.showtime` 定义值 |
|
||||
| 4 | `C_Player`/`Desk.PlayerList[seat]` 双镜像在哪些方法里被读写,确保 `PlayerStore` 单一来源无遗漏 | grep `SetDeskInfo`/`getDeskInfo`/`PlayerList[` 全调用点 |
|
||||
@@ -0,0 +1,365 @@
|
||||
# legacy-layer → Cocos prefab 转换脚本设计
|
||||
|
||||
> **Status**: Draft v1 (brainstorming approved, awaiting user spec review)
|
||||
> **Date**: 2026-09-02
|
||||
> **Spec for**: `scripts/legacy-layer-to-cocos-prefab.mjs` + 测试套件
|
||||
|
||||
## Goal
|
||||
|
||||
把 `projects/Game_Surface_3/save/Layer*.xml`(GB18030 编码 XML)+ `output/gameabc_*.json` 的结构化 UI 数据,**程序化转换为 Cocos prefab JSON**,让 YouleNexus 已迁移的 ~17 个组件 prefab(widgets/ + views/ + Login_Layer)能 1:1 对齐原项目 Game_Surface_3 的布局+精灵+Widget 锚定。
|
||||
|
||||
**成功标准**:脚本生成的 prefab 加载到 Cocos 编辑器 0 报错,节点树/SpriteFrame/Widget 锚定与原 Layer XML 字段语义一致。
|
||||
|
||||
## 范围(本设计覆盖)
|
||||
|
||||
### 已核实映射表(2026-09-02 全量 XML 解析 + 用户拍板)
|
||||
|
||||
| 目标 prefab(YouleNexus) | 对应 Layer XML | Layer 名称 | Group | Spirit 数 | 备注 |
|
||||
|---|---|---|---|---|---|
|
||||
| `prefabs/Login_Layer.prefab` | Layer00002.xml | Login_Layer | 2 | 18 | 已有完整复刻,端到端 PoC 验证 |
|
||||
| `prefabs/views/PlayerInfoView.prefab` | **Layer00017.xml** | **MenuPlayerInfo_Layer** | **18** | 15 | **用户拍板 A** |
|
||||
| `prefabs/widgets/Group106_PhoneVerify.prefab` | Layer00029.xml | phoneInfo | 106, 108 | 38 | |
|
||||
| `prefabs/widgets/Layer10_Protocol.prefab` | Layer00010.xml | Protol_Layer | 37 | 4 | **PoC 阶段 2**(含 Widget 锚定) |
|
||||
| `prefabs/widgets/Layer609_Setting.prefab` | Layer00609.xml | Setting_Layer | 6 | 15 | |
|
||||
| `prefabs/widgets/Layer612_Tips.prefab` | Layer00612.xml | Tips_Layer | 32, 89 | 5 | |
|
||||
| `prefabs/widgets/Layer613_RoomCardUpdate.prefab` | Layer00613.xml | updateRoomCard_Layer | 34 | 3 | |
|
||||
| `prefabs/widgets/Layer614_Loading.prefab` | Layer00614.xml | Loading_Layer | 40 | 2 | |
|
||||
| `prefabs/widgets/Layer615_Reconnect.prefab` | Layer00615.xml | Reconnect_Layer | 35 | 4 | |
|
||||
| `prefabs/widgets/Layer616_Kick.prefab` | Layer00616.xml | Kick_Layer | 33 | 3 | |
|
||||
| `prefabs/widgets/Layer617_Notice.prefab` | **Layer00008.xml** | **Notice_Layer** | **3, 95** | **9** | **用户拍板 B** |
|
||||
| `prefabs/widgets/Layer619_BackOtherGame.prefab` | Layer00619.xml | BackHall_Layer | 55 | 1 | Layer 名 BackHall ≠ BackOtherGame,但 group 唯一匹配 |
|
||||
| `prefabs/widgets/Layer620_Record.prefab` | Layer00620.xml | recordLayer | 101 | 3 | |
|
||||
|
||||
**总计:11 个 prefab 全部映射到明确 Layer XML**(不含 6 个 NO_MATCH widget)
|
||||
|
||||
### 范围外(**用户拍板 C:先不做**)
|
||||
|
||||
| 目标 prefab | 原因 |
|
||||
|---|---|
|
||||
| `prefabs/templates/ListItem_Room.prefab` | 跨 Layer 复用的 widget 子模板 / YouleNexus 新加,未在 save/Layer*.xml 中找到 |
|
||||
| `prefabs/widgets/Btn_Primary.prefab` | 通用按钮 widget,YouleNexus 新加 |
|
||||
| `prefabs/widgets/IconButton.prefab` | 通用图标按钮 widget,YouleNexus 新加 |
|
||||
| `prefabs/widgets/Modal.prefab` | 通用弹窗 widget,YouleNexus 新加 |
|
||||
| `prefabs/widgets/NumericLabel_Score.prefab` | 数字滚动 Label widget,YouleNexus 新加 |
|
||||
| `prefabs/widgets/ProgressBar_Standard.prefab` | 通用进度条 widget,YouleNexus 新加 |
|
||||
|
||||
### 事实源
|
||||
|
||||
完整 Layer 解析数据见 `docs/superpowers/data/layer-spirit-summary.json`(68 个 Layer 全量、81 个 group、5 种 SpiritType 分布)。
|
||||
|
||||
### 未来扩展(如需)
|
||||
|
||||
如果要补做 6 个 NO_MATCH widget,需先在 `scripts/legacy-layer-to-cocos-prefab.mjs` 加"跨 Layer Spirit 提取"能力(按 Sprite name / Layer 内 widget 模式匹配),不在本设计范围。
|
||||
|
||||
**不在范围**:
|
||||
- 未迁移的 68 个 Layer 中的其余 ~50 个
|
||||
- 动画(Spirit 的 `ani_*` 属性 / Spine / DragonBones)
|
||||
- 多语言文本
|
||||
- 协议层字段
|
||||
|
||||
## Architecture
|
||||
|
||||
### 数据流
|
||||
|
||||
```
|
||||
projects/Game_Surface_3/save/Layer*.xml ─┐
|
||||
projects/Game_Surface_3/output/ ─┼→ [legacy-layer-to-cocos-prefab.mjs] → out/legacy-migration/
|
||||
gameabc_Image.json (sprite 映射) │ ├─ <Name>.prefab (UTF-8 JSON 数组)
|
||||
gameabc_Project.json (适配元数据) │ └─ <Name>.diff (vs YouleNexus 现有)
|
||||
projects/Game_Surface_3/js/ ─┘
|
||||
gameabc.min.js (语义来源,需逆向)
|
||||
```
|
||||
|
||||
### 模块划分(单文件脚本 + 7 个测试)
|
||||
|
||||
| 模块 | 职责 |
|
||||
|---|---|
|
||||
| `readLayerXml(path)` | GB18030 编码读 XML → JS 对象 |
|
||||
| `resolveSpriteFrameUuid(imgResId)` | 查 gameabc_Image.json → atlas SpriteFrame 真 UUID(含 @f9941) |
|
||||
| `buildGroupParents(spirits)` | 按 BelongGroupID 分组 → group-X 父节点骨架 |
|
||||
| `mapSpiritTypeToComponent(type)` | SpiritType → 组件类型枚举(逆向 gameabc.min.js) |
|
||||
| `computeLpos(spirit, parentGroup)` | CoordSelfPos + CoordRelaX/Y → _lpos {x,y,z} |
|
||||
| `computeContentSize(spirit)` | SizeCalcMode + SizeRelaWidth/Height + Width/HeightCalcMode → _contentSize {width,height} |
|
||||
| `computeWidgetAlignment(spirit)` | SizeCalcMode + 父 group 关系 → cc.Widget._alignFlags + _horizontalCenter 等 |
|
||||
| `emitPrefabJson(name, group, root)` | 按 Login_Layer.prefab schema 输出 JSON 数组(含 CompPrefabInfo / PrefabInfo) |
|
||||
| `diffAgainstExisting(generated, existing)` | 输出 .diff 文件 |
|
||||
|
||||
## 输入契约
|
||||
|
||||
### `save/Layer<id>.xml`
|
||||
|
||||
**GB18030 编码**(不是 UTF-8 也不是 GBK!实测 Python xml.etree 用 GBK 解码会乱码,GB18030 才能完整解析中文 Spirit 名)。结构:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Layer Version="2.6" SaveTime="...">
|
||||
<PropertyList>
|
||||
<Property Name="ID" Value="2"/>
|
||||
<Property Name="Name" Value="Login_Layer"/>
|
||||
<Property Name="Description" Value=""/>
|
||||
</PropertyList>
|
||||
<OptionList/> <EventList/>
|
||||
<SpiritList>
|
||||
<Spirit>
|
||||
<PropertyList>
|
||||
<Property Name="SpiritType" Value="0"/>
|
||||
<Property Name="ID" Value="<spirit_id>"/>
|
||||
<Property Name="Name" Value="<spirit_name_zh>"/> <!-- GB18030 -->
|
||||
<Property Name="ImgResID" Value="<sprite_id>"/>
|
||||
<Property Name="X" Value="0"/>
|
||||
<Property Name="Y" Value="0"/>
|
||||
<Property Name="Width" Value="1280"/>
|
||||
<Property Name="Height" Value="720"/>
|
||||
<Property Name="BelongLayerID" Value="2"/>
|
||||
<Property Name="IndexOfLayer" Value="1"/>
|
||||
<Property Name="BelongGroupID" Value="2"/> <!-- 0=根, >0=group-N -->
|
||||
<Property Name="CoordParentID" Value="0"/>
|
||||
<Property Name="CoordParentPos" Value="0"/>
|
||||
<Property Name="CoordSelfPos" Value="0"/>
|
||||
<Property Name="CoordRelaX" Value="0"/>
|
||||
<Property Name="CoordRelaY" Value="0"/>
|
||||
<Property Name="SizeCalcMode" Value="2"/>
|
||||
<Property Name="SizeParentID" Value="21"/>
|
||||
<Property Name="WidthCalcMode" Value="1"/>
|
||||
<Property Name="SizeRelaWidth" Value="10000"/>
|
||||
<Property Name="HeightCalcMode" Value="1"/>
|
||||
<Property Name="SizeRelaHeight" Value="10000"/>
|
||||
</PropertyList>
|
||||
<OptionList/> <EventList/>
|
||||
</Spirit>
|
||||
...
|
||||
</SpiritList>
|
||||
</Layer>
|
||||
```
|
||||
|
||||
> **GB18030 编码陷阱**:脚本必须用 GB18030 读(GBK 是 GB18030 子集,但部分生僻字 / 私有区字符需要 GB18030 才能解码)。UTF-8 会乱码。Spirit 名是中文,输出 .prefab JSON 时按 UTF-8 写。Node.js 用 `iconv-lite` 包支持 GB18030 解码。
|
||||
|
||||
### `output/gameabc_Image.json`
|
||||
|
||||
`ImgResID → atlas spriteFrame path` 映射(具体 schema 在 PoC 阶段 1 解析)。
|
||||
|
||||
### `output/gameabc_Project.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"Property": {
|
||||
"ProjectName": "Game_Surface_3",
|
||||
"ScreenWidth": 1280,
|
||||
"ScreenHeight": 720,
|
||||
"GameSceneWidth": 1699,
|
||||
"GameSceneHeight": 1800,
|
||||
"ScreenFitMode": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **适配策略**:原项目 ScreenWidth=1280, ScreenHeight=720,但 YouleNexus 当前 Login_Layer.prefab 根 _contentSize = 1600×720。脚本默认生成 1600×720 的根 UITransform(与 Login_Layer.prefab 一致),但 Spirit 内部坐标按原 XML 不变——**Widget 锚定负责多分辨率适配**。
|
||||
|
||||
### `js/gameabc.min.js`
|
||||
|
||||
逆向目标:
|
||||
- `SpiritType` 枚举(0=Sprite?1=Text?2=Button?3=ProgressBar?...)
|
||||
- `CoordSelfPos` 计算公式(X/Y 相对父节点的偏移量)
|
||||
- `SizeCalcMode` 公式(SizeParentID / SizeRelaWidth / SizeRelaHeight 怎么合成 _contentSize)
|
||||
- `WidthCalcMode` / `HeightCalcMode` 公式
|
||||
- BelongGroupID 的语义
|
||||
|
||||
> **逆向策略**:在 PoC 阶段 1 之前完成。逆向结果固化到 `framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json` 作为已知事实源。
|
||||
|
||||
## 输出契约
|
||||
|
||||
### `out/legacy-migration/<Name>.prefab`
|
||||
|
||||
UTF-8 JSON 数组(**严格按 Login_Layer.prefab schema**),字段含义:
|
||||
|
||||
```typescript
|
||||
type PrefabJson = Array<PrefabObject>
|
||||
type PrefabObject =
|
||||
| { __type__: "cc.Prefab", _name: string, ... } // 0
|
||||
| { __type__: "cc.Node", _name: string, _children: [{__id__: number}], _components: [{__id__: number}], _lpos: Vec3, _lrot: Quat, _lscale: Vec3, ... }
|
||||
| { __type__: "cc.UITransform", _contentSize: Size, _anchorPoint: Vec2, ... }
|
||||
| { __type__: "cc.Sprite", _spriteFrame: { __uuid__: string, __expectedType__: "cc.SpriteFrame" }, _type: 0|1, _sizeMode: 0|1|2, ... }
|
||||
| { __type__: "cc.Label", _string: string, _fontSize: number, _horizontalAlign: 0|1|2, _verticalAlign: 0|1|2, _color: Color, _font: null, _isSystemFontUsed: true, ... }
|
||||
| { __type__: "cc.Widget", _alignFlags: number, _left?: number, _right?: number, _top?: number, _bottom?: number, _horizontalCenter?: number, _verticalCenter?: number, _isAbs*: true, _alignMode: 2, ... }
|
||||
| { __type__: "cc.CompPrefabInfo", fileId: string }
|
||||
| { __type__: "cc.PrefabInfo", root: {__id__: number}, asset: {__id__: number}, fileId: string, ... }
|
||||
| { __type__: string /* custom class UUID */, _name: "", _enabled: true, ... }
|
||||
```
|
||||
|
||||
### `out/legacy-migration/<Name>.diff`
|
||||
|
||||
`diff -u` 格式,对比脚本输出 vs YouleNexus `prefabs/<path>/<Name>.prefab` 现有内容。**仅供人审**,不自动覆盖。
|
||||
|
||||
### 输出根节点 `_contentSize`
|
||||
|
||||
默认 `1600×720`(与 Login_Layer.prefab 一致),**不按原 Project.GameSceneWidth/Height**——理由:YouleNexus Canvas 设计尺寸固定 1600×720(CLAUDE.md 第一准则精神:UI 层自己处理适配,原项目 ScreenWidth 是另一回事)。
|
||||
|
||||
## 关键映射规则
|
||||
|
||||
### Spirit → cc.Node
|
||||
|
||||
| 原字段 | Cocos 字段 |
|
||||
|---|---|
|
||||
| `Spirit.Name` | `cc.Node._name` |
|
||||
| `Spirit.ID` | 用于生成 `_id`(cocos 内部 ID,与 fileId 不同) |
|
||||
| `Spirit.BelongGroupID = "0"` | 节点挂在 Layer 根节点下 |
|
||||
| `Spirit.BelongGroupID = "N"` (N>0) | 节点挂在 `group-N` 父节点下 |
|
||||
| `Spirit.IndexOfLayer` | 同 group 内的兄弟顺序(生成 `_children` 数组顺序) |
|
||||
| `Spirit.CoordParentID` | 父节点引用(=同 group 内其他 Spirit 的 ID) |
|
||||
|
||||
### Group 父节点自动生成
|
||||
|
||||
- 自动创建 `cc.Node _name = "group-<N>"`,N = 原 BelongGroupID 值
|
||||
- **保留全局编号**(不重新编号为 group-1/2/3),便于 cross-layer 调试(与 Login_Layer 复刻一致)
|
||||
- 父节点本身**无 sprite/label**,只挂 UITransform + Widget(与 Login_Layer.prefab 的 group-2 一致)
|
||||
- 多个 group 时按 group ID 升序排(group-1, group-2, ...)
|
||||
|
||||
### SpiritType → 组件
|
||||
|
||||
**逆向目标**(在 PoC 阶段 1 之前完成)。当前猜测:
|
||||
|
||||
| SpiritType | 组件类型 | 说明 |
|
||||
|---|---|---|
|
||||
| 0 | `cc.Sprite` | 普通精灵 |
|
||||
| 1 | `cc.Label` | 文本 |
|
||||
| 2 | `cc.Button` | 按钮(待确认) |
|
||||
| 3 | `cc.ProgressBar` | 进度条(Login_Layer "进度条底" 是 type=3) |
|
||||
| ... | ... | 全部逆向 gameabc.min.js 后填表 |
|
||||
|
||||
**未识别 type → throw**,不静默。
|
||||
|
||||
### ImgResID → SpriteFrame 真 UUID
|
||||
|
||||
查 `output/gameabc_Image.json` 拿到 atlas 图片路径 → 查 `atlas-hall/atlas-login/atlas-room` atlas 的 SpriteFrame UUID(含 `@f9941` sub-asset 后缀)。
|
||||
|
||||
> **找不到 → throw + 列出所有未映射 ID**(CLAUDE.md 第二准则)。
|
||||
|
||||
### 坐标/尺寸计算
|
||||
|
||||
**锚点转换公式(实测验证 2026-09-02,Login_Layer 渠道logo + 游客登录双节点匹配)**:
|
||||
|
||||
原项目精灵锚点 = **左上角**(X/Y 是精灵左上角的绝对屏幕坐标,基于 1280×720)
|
||||
Cocos 精灵锚点 = **中心**(_lpos 是精灵中心在父节点坐标系的位置,Y-up)
|
||||
|
||||
```
|
||||
# Spirit → cc.Node._lpos(假设父节点 _contentSize 1600×720、anchorPoint (0.5, 0.5))
|
||||
cocos_lpos.x = original_X + original_W / 2 - 640 # 原坐标系半宽 1280/2
|
||||
cocos_lpos.y = 360 - (original_Y + original_H / 2) # Y-up 翻转 + 原坐标系半高 720/2
|
||||
cocos_lpos.z = 0
|
||||
```
|
||||
|
||||
**验证证据**:
|
||||
- 渠道logo:原 (734, 143, 59, 148) → Cocos (763.5 - 640, 360 - 217) = (123.5, 143) ✓
|
||||
- 游客登录:原 (540, 388, 320, 95) → Cocos (700 - 640, 360 - 435.5) = (60, -75.5) ✓
|
||||
|
||||
**SizeCalcMode 公式(待 PoC 阶段 1 逆向 gameabc.min.js)**:
|
||||
|
||||
```
|
||||
_contentSize.width = Spirit.Width || 0
|
||||
_contentSize.height = Spirit.Height || 0
|
||||
|
||||
# 如果 SizeCalcMode != 0,用 SizeParentID + SizeRelaWidth/Height 计算(公式逆向中)
|
||||
if Spirit.SizeCalcMode != 0:
|
||||
parent_size = lookup(Spirit.SizeParentID)
|
||||
_contentSize.width = parent_size.width * Spirit.SizeRelaWidth / 10000
|
||||
_contentSize.height = parent_size.height * Spirit.SizeRelaHeight / 10000
|
||||
```
|
||||
|
||||
**Widget 锚定**:
|
||||
|
||||
| 情况 | `_alignFlags` | 其他字段 |
|
||||
|---|---|---|
|
||||
| 节点相对 Canvas 全屏铺满 | 45 (= 左+右+上+下) | `_left/_right/_top/_bottom = 0` |
|
||||
| 节点水平居中 | 16 (= 水平居中) | `_horizontalCenter = Spirit.X` |
|
||||
| 节点垂直居中 | 8 | `_verticalCenter = Spirit.Y` |
|
||||
| 节点左上对齐 | 9 (= 左+上) | `_left = Spirit.X, _top = Spirit.Y` |
|
||||
| ... | ... | 全部逆向后填表 |
|
||||
|
||||
**逆向不出公式 → throw**,不兜底默认值。
|
||||
|
||||
## 错误处理(CLAUDE.md 第二准则:不静默兜底)
|
||||
|
||||
| 场景 | 处理 |
|
||||
|---|---|
|
||||
| `gameabc_Image.json` 找不到 ImgResID | `throw new Error(\`ImgResID ${id} (Spirit ${sid}) not in gameabc_Image.json\`)` |
|
||||
| SpiritType 不在已知枚举 | `throw new Error(\`Unknown SpiritType ${type} (Spirit ${sid}) — reverse gameabc.min.js first\`)` |
|
||||
| CoordSelfPos/SizeCalcMode 公式逆向不出 | `throw new Error(\`Cannot compute coord/size for Spirit ${sid}: formula unknown\`)` |
|
||||
| XML 字段缺失或编码错 | `throw new Error(\`Spirit ${sid} missing field ${fieldName}\`)` |
|
||||
| 输出目录有同名 .prefab | **覆盖**(隔离目录是 transient)+ stdout 列 diff 路径 |
|
||||
| `gameabc_Image.json` schema 变更 | 启动时 schema 校验,失败直接 throw |
|
||||
| Layer 在 PoC 阶段 4 找不到目标 YouleNexus prefab | throw + 列出所有"无目标"Layer,让用户补映射 |
|
||||
|
||||
## 测试策略(TDD)
|
||||
|
||||
按 ui-asset-and-skin plan 风格:`node:test` + `node:assert` + `tsx`,零第三方依赖。
|
||||
|
||||
### 测试套件:`framework-tests/legacy-layer-migration/`
|
||||
|
||||
| 文件 | 覆盖 |
|
||||
|---|---|
|
||||
| `xml-parser.test.mjs` | GB18030 编码读、字段提取、BelongGroupID 分组 |
|
||||
| `spirit-to-node.test.mjs` | SpiritType=0/1/3 → Sprite/Text/ProgressBar 组件映射 |
|
||||
| `group-builder.test.mjs` | BelongGroupID → group-X 父节点合并 + 同 group 内 IndexOfLayer 排序 |
|
||||
| `coord-calculator.test.mjs` | CoordSelfPos/SizeCalcMode/WidthCalcMode/HeightCalcMode 公式正确性 |
|
||||
| `sprite-uuid-resolver.test.mjs` | ImgResID → 现有 atlas SpriteFrame UUID(基于真实 gameabc_Image.json fixture) |
|
||||
| `prefab-emitter.test.mjs` | 输出 JSON 格式严格按 Login_Layer.prefab schema(用 Login_Layer.prefab 实际内容作 fixture) |
|
||||
| `poc-iconbutton.test.mjs` | 端到端:输入 save/Layer*.xml → 输出 .prefab → 与 YouleNexus 原 prefab 做 schema 对比(**不对比内容,只对比结构**) |
|
||||
|
||||
### TDD 红绿循环(每个 PoC 阶段)
|
||||
|
||||
1. **红**:写测试(输入已知 Layer XML,期望已知 prefab JSON 结构)
|
||||
2. **绿**:写脚本直到测试通过
|
||||
3. **重构**:优化代码
|
||||
4. **人工 diff**:`diff -u out/legacy-migration/<Name>.prefab prefabs/<path>/<Name>.prefab`,看 diff
|
||||
5. **修订**:根据 diff 调整映射规则,回到 1
|
||||
|
||||
### 测试 fixtures
|
||||
|
||||
- `framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json` — gameabc.min.js 逆向结果固化
|
||||
- `framework-tests/legacy-layer-migration/fixtures/gameabc_Image.json` — 真实 gameabc_Image.json 副本
|
||||
- `framework-tests/legacy-layer-migration/fixtures/Layer00002.xml` — Login_Layer XML 副本(GB18030)
|
||||
- `framework-tests/legacy-layer-migration/fixtures/Login_Layer.prefab` — 已复刻的 Login_Layer prefab 副本(用于端到端对比)
|
||||
|
||||
## PoC 路径(4 阶段)
|
||||
|
||||
| 阶段 | 目标 | 验证点 |
|
||||
|---|---|---|
|
||||
| **阶段 1** | IconButton(单 Sprite、无 Widget、无 group) | Spirit→Node + SpriteFrame 解析 + JSON 输出 + schema 对比 |
|
||||
| **阶段 2** | Layer10_Protol(Layer00010,group 37,含 Widget 锚定) | group-X 父节点合并 + Widget 锚定逻辑 |
|
||||
| **阶段 3** | Login_Layer(已有完整 1:1 复刻可对比) | 端到端验证 + 人工 diff 接近零差异 |
|
||||
| **阶段 4** | 批量跑剩 14 个 | 每个输出 .diff 供人审 |
|
||||
|
||||
阶段 1 必须先逆向 gameabc.min.js 中 IconButton 用到的 SpiritType/Coord 公式。
|
||||
|
||||
## 不做的事(明确边界)
|
||||
|
||||
- ❌ 不覆盖 YouleNexus 原 prefab(**只生成候选 + diff**)
|
||||
- ❌ 不处理动画(Spirit 的 `ani_*` 属性 → throw + 标出该 Spirit)
|
||||
- ❌ 不处理 Spine / DragonBones(unknown SpiritType → throw)
|
||||
- ❌ 不处理多语言(只读 `_string` 字面值,不查国际化表)
|
||||
- ❌ 不迁移协议字段(CLAUDE.md 第一准则:协议不动)
|
||||
- ❌ 不重写为 click handler / 业务逻辑(只做布局+精灵迁移,**业务挂脚本留 follow-up**)
|
||||
- ❌ 不处理 OptionList.tag/tag1-3 / EventList(除非 layer-to-prefab 有明确映射,待逆向)
|
||||
|
||||
## Risks & Open Questions
|
||||
|
||||
1. **gameabc.min.js 逆向深度**:SpiritType 枚举只有 5 种(0/1/3/4/5,全量解析已确认),但 SpiritType 4 和 5 的语义仍需逆向(猜测 4=Particle / 5=Button,但需 PoC 阶段 1 验证);CoordSelfPos / SizeCalcMode 公式逆向中。
|
||||
2. **SpriteFrame UUID 跨 atlas 选择规则**:atlas 选哪个取决于原 Layer 来源场景 —— Login_Layer 用 `atlas-login`,MainScene 用 `atlas-hall`,Room 用 `atlas-room`。具体规则按 Layer 编号或 Spirit 名称模式决定,**PoC 阶段 1 实测后固化到 fixtures/spirit-semantics.json**。
|
||||
3. **Layer00007 ewm_Layer 含 group=0**:表示有节点不在 group-X 父节点下(直接挂在 Layer 根)。脚本需支持 BelongGroupID=0 节点(按现有 group-X 父节点方案应直接挂 Layer 根)—— **已在范围表规则中说明**,待 PoC 验证。
|
||||
4. **YouleNexus 原 prefab 的偏差**:某些 prefab 可能已经人工调整过布局,与原 Layer 不一致 —— **diff 工具帮人审,但最终覆盖决策在你**
|
||||
|
||||
## Out of Scope(follow-up)
|
||||
|
||||
- 把所有 68 个 Layer 全迁移
|
||||
- 自动生成 sprite atlas(走 ui-asset-and-skin plan Task 3 独立推进)
|
||||
- 迁移 OptionList.tag/tag1-3 业务字段
|
||||
- 迁移 EventList 事件处理
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- 17 个目标 prefab 全部生成;与 YouleNexus 现有版本 diff **仅显示手工已修字段的差异**(坐标/尺寸/spriteFrame UUID/Widget 锚定全部精确匹配)
|
||||
- 锚点转换公式精度:**至少 3 个已知 Spirit 的 _lpos 与人工复刻版 prefab 误差 < 0.001**
|
||||
- 7 个测试文件全部通过(node:test)
|
||||
- `framework-tests/legacy-layer-migration/` 覆盖率 ≥ 80%
|
||||
- 脚本可在 CI 跑(无 GUI 依赖)
|
||||
- gameabc.min.js 逆向结果固化到 fixtures/spirit-semantics.json,作为后续 PoC 阶段的事实源
|
||||
Reference in New Issue
Block a user