docs(spec): 原工程 Game_Surface_3 数据模型 (7 个 JSON schema + Layer XML + ObjectType 映射)

基于 projects/Game_Surface_3/{output/*.json, save/*.xml, js/gameabc.min.js} 全面阅读.
Layer 8 (Notice_Layer) ObjectList 与 save/Layer00008.xml 的 9 个 Spirit ID 完全 1:1 验证通过.

实测 ObjectType 映射 (修正之前误判):
- ObjectType=2 = Image/Sprite (含 ImageFileID, FrameIndex, L9/T9/R9/B9)
- ObjectType=4 = EditBox (含 Text, FontSize, FontColor) — 不是 Label!
- ObjectType=0/1/3/5 需 gameabc.min.js 逆向确认

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-03 07:37:36 +08:00
co-authored by Claude Opus 5
parent c2a7a071fd
commit 00d6ba3fc4
@@ -0,0 +1,360 @@
# 原工程数据模型 (Game_Surface_3)
> **Status**: Draft v1 (2026-09-03)
> **Purpose**: 为后续 UI 迁移提供 Game_Surface_3 数据契约的权威参考
> **基于**: `projects/Game_Surface_3/output/*.json` + `save/*.xml` + `js/gameabc.min.js`(解析输出)
## 总览
Game_Surface_3 是 Cocos-JS (HTML5) 单页游戏平台,使用以下数据文件:
| 文件 | 大小 | 内容 |
|---|---|---|
| `output/gameabc_Project.json` | 778 B | 全局配置(屏幕尺寸、事件名、fps 等) |
| `output/gameabc_GroupList.json` | 8.3 KB | Group→Object 列表(55 groups) |
| `output/gameabc_Layer.json` | 12 KB | Layer→Object 列表(55 layers) |
| `output/gameabc_Object.json` | 905 KB | **核心**:991 个 object 的完整属性+事件+选项(3293 个槽位) |
| `output/gameabc_Image.json` | 67 KB | 439 张 sprite PNG 元数据(id → bmp + 多帧) |
| `output/gameabc_Voice.json` | 252 B | 音频文件映射(id → mp3) |
| `output/gameabc_GameTxt.json` | 20 KB | 多语言文案(GameTxtList) |
| `save/*.xml` | 59 文件 | Layer XML 定义(每层一个文件) |
| `js/gameabc.min.js` | 185 KB | 引擎核心(已 minify,难逆向) |
**Layer 8 (Notice_Layer) 关键交叉验证**:
- `gameabc_Layer.json[8].ObjectList = [3122, 145, 3121, 3017, 5, 235, 604, 3057, 15]` (9 个 object IDs)
- `save/Layer00008.xml` 的 9 个 `<Spirit ID="...">` IDs:**完全匹配**
- `gameabc_Object.json` 中这 9 个 ID 的 `GroupID = 3`(145)或 `GroupID = 95`(其他 8 个)也**完全匹配** XML 的 `BelongGroupID` 字段
## gameabc_Project.json schema
```typescript
type Project = {
Property: {
ProjectName: string; // e.g. "Game_Surface_3"
ScreenWidth: number; // 1280
ScreenHeight: number; // 720
GameSceneWidth: number; // 1699 (logical scene width)
GameSceneHeight: number; // 1800
ScreenFitMode: number; // 1 (适配模式)
TcpIP: string;
TcpPort: string;
Http: string;
title: string;
};
Event: {
gamestart, gamebegindraw, gameenddraw,
mousedown, mousedown_nomove, mouseup, mousemove,
gamemydraw, gamemydrawbegin, chongzhi,
tcpconnected, tcpdisconnected, tcpmessage, tcperror, httpmessage,
ani_doend, box_doend, onresize, ontimer, onloadurl: number;
};
Option: {
fps: number; // 30
tag, tag1, tag2, tag3: number;
showmodel: number;
};
};
```
## gameabc_Layer.json schema
```typescript
type Layer = {
Property: {
LayerID: number; // 1-620 (不连续)
LayerName: string; // e.g. "Login_Layer", "Notice_Layer"
};
ObjectList: number[]; // 该 layer 包含的 object ID 列表(按 IndexOfLayer 排序)
};
type LayerList = (Layer | {})[]; // 长度 621,空对象占位
```
**示例**:
```json
{"Property":{"LayerID":8,"LayerName":"Notice_Layer"},"ObjectList":[3122,145,3121,3017,5,235,604,3057,15]}
```
## gameabc_GroupList.json schema
```typescript
type Group = {
GroupID: number; // 1, 3, 95, ...(55 个有效 group)
ObjectList: number[]; // 该 group 包含的 object ID 列表
};
type GroupList = (Group | {})[]; // 长度未读(稀疏)
```
**关键映射关系**:`Object.GroupID ↔ Object.ObjectList in Layer.ObjectList`
## gameabc_Object.json schema(**核心**)
```typescript
type GameObject = {
Property: { /* 共 41 字段,见下方 */ };
Event?: { /* 事件→回调 object ID 映射 */ };
Option?: { /* 选项 11 字段 */ };
};
type ObjectList = (GameObject | {})[]; // 长度 3293,索引 = object ID - 1
```
### Property 字段(41 字段)
#### 标识 + 位置(10 字段)
| 字段 | 类型 | 含义 | XML 对应 |
|---|---|---|---|
| `ObjectID` | int | 唯一 ID(也是数组索引+1) | Spirit `ID` 属性 |
| `ObjectType` | int | 组件类型(见下方映射表) | Spirit `SpiritType` |
| `ObjectName` | str | 中文/英文节点名 | Spirit `Name` |
| `Left`, `Top` | int | 绝对坐标 X, Y(左上角锚点) | Spirit `X`, `Y` |
| `Width`, `Height` | int | 尺寸 W, H | Spirit `Width`, `Height` |
| `BelongLayerID` | int | 所属 Layer ID | Spirit `BelongLayerID` |
| `IndexOfLayer` | int | 同 layer 内排序 | Spirit `IndexOfLayer` |
| `GroupID` | int | 所属 Group(0=根 layer) | Spirit `BelongGroupID` |
| `offX`, `offY` | int | 同 Left/Top(冗余字段) | — |
#### ObjectType 映射(实测)
| ObjectType | 含义 | 出现次数 (Notice_Layer 9 个中) | 示例 |
|---|---|---|---|
| 0 | (未观察到) | — | — |
| 1 | (未观察到) | — | — |
| **2** | **Image/Sprite** | **8/9** (Notice_Layer 中除 145 外) | id 5 (通知), id 3122 (Bgsprite), id 3121 (按钮托盘 SLICED) |
| 3 | (未观察到?) | — | — |
| **4** | **EditBox (含 Text + FontSize + FontColor)** | **1/9** | id 145 (公告内容) |
| 5 | (未观察到) | — | — |
> ⚠️ **修正之前判断**:之前我以为 ObjectType 2=Sprite, 3=ProgressBar, 4=EditBox, 5=SLICED Sprite(基于 gameabc.min.js minified 推断)—— **实测 Object 145 是 ObjectType=4(EditBox),含 Text/FontSize/FontColor 字段**,不是纯 Label!需要重新校验 Cocos 端 cc.EditBox vs cc.Label 对应关系。
#### 9-slice border 字段(4 字段)
| 字段 | 类型 | 含义 |
|---|---|---|
| `L9`, `T9`, `R9`, `B9` | int | 左/上/右/下 9-slice border 像素值 |
> 🟢 **Cocos 对应**:`cc.SpriteFrame.capInsets = [L9, T9, R9, B9]`(capInsets 全 0 = SIMPLE,否则 SLICED)
#### Frame 字段(4 字段)
| 字段 | 类型 | 含义 |
|---|---|---|
| `FrameStyle` | int | 0=单帧, 1=多帧(具体待 gameabc.min.js 逆向) |
| `FrameIndex` | int | 当前显示的帧索引(0 到 FrameStyle 总帧数-1) |
| `TextFrames` | str | (空字符串或具体多帧配置?需逆向) |
> 🟡 **Frame 字段语义待 gameabc.min.js 逆向确认**
#### 字体字段(EditBox/Text 字段,8 字段)
| 字段 | 类型 | 含义 |
|---|---|---|
| `FontSize` | int | 字体大小 |
| `FontBold` | int | 0/1 |
| `FontColorR/G/B` | int | RGB 颜色分量 |
| `FontColor` | str | "#FFFFFF" 形式 |
| `BackColorR/G/B/A` | int | 背景色(带 alpha) |
| `BackColor` | str | 背景色 16进制 |
| `GameTxtStyle` | int | 多语言文案索引(对应 gameabc_GameTxt.json) |
| `LineSpace` | int | 行距 |
| `Text` | str | 文本内容 |
#### 关系字段(4 字段)
| 字段 | 类型 | 含义 |
|---|---|---|
| `Parent` | int | 父 object ID(0=无父) |
| `OriginID` | int | 锚点参考 object ID(0=绝对坐标) |
| `OriginPos` | int | 锚点位置:1=top-left, 9=center |
| `SelfPos` | int | 自身位置:1=top-left, 9=center |
#### 资源引用字段(2 字段)
| 字段 | 类型 | 含义 | Cocos 对应 |
|---|---|---|---|
| `ImageFileID` | int | sprite 资源 ID(→ gameabc_Image.json) | `_spriteFrame.__uuid__` |
| `VoiceFileID` | int | 音效 ID(→ gameabc_Voice.json) | (audio component) |
#### 杂项(4 字段)
| 字段 | 类型 | 含义 |
|---|---|---|
| `Data` | str | 自定义数据字段 |
| `TimerInterval` | int | 定时器间隔(0=无) |
| `L9/T9/R9/B9` | (见上) | (见上) |
### Event schema
```typescript
type Event = {
mousedown?: number; // 回调 object ID(值=0 表示无回调)
mouseup?: number;
mousemove?: number;
ontimer?: number;
// 注: Project.json 列出 20+ 事件类型, Object 的 Event 只观察到这 4 个常用
};
```
> 🟡 Project.json Event 列出 20+ 种 (gamestart, gamebegindraw, ...),但 Object.json 里只观察到 mousedown/mouseup/mousemove/ontimer 4 种。其他事件可能由 Project 级或 Group 级处理。
### Option schema(11 字段)
```typescript
type Option = {
tag, tag1, tag2, tag3: number; // 用户自定义标签
vx, vy, vw, vh: number; // velocity (动画/物理)
canclick: number; // 0/1, 是否可点击
visbale: number; // 0/1, 是否可见(注意:原工程拼写错误 "visbale" 而非 "visible"!)
};
```
## gameabc_Image.json schema
```typescript
type ImageRecord = {
id: number; // = ImgResID(与 Object.ImageFileID 对应)
w_all: number; // 完整 sprite 宽(含多帧拼接)
h_all: number; // 完整 sprite 高
w: number; // 帧宽
h: number; // 帧高
frame_all: number; // 总帧数
bmp: string; // "00001.png" 形式
w1: number; // 第 1 帧宽(多帧时 w 可能 != w1)
h1: number; // 第 1 帧高
};
type ImageFileList = (ImageRecord | {})[]; // 长度 440,索引 = id - 1
```
**多帧 sprite 迁移规则**(已写入 playbook §关键迁移规则):
- `id=N` 单帧:`bmp = "0000N.png"` 直接用
- `id=N` 多帧(frame_all > 1):`bmp = "0000N.png"` 含多帧 → Cocos 用 `0000N_01.png` ~ `0000N_NN.png` 多张散图
## gameabc_Voice.json schema
```typescript
type VoiceRecord = { id: number; file: string; }; // file = "0000N.mp3"
type VoiceFileList = (VoiceRecord | {})[];
```
## gameabc_GameTxt.json schema
```typescript
type GameTxtList = (string | {})[]; // 长度 ≈ 274,多语言文案槽位
```
**用法**:`Object.GameTxtStyle` 是 GameTxtList 的索引 → 取字符串作为显示文本(支持多语言)。
## Layer XML ↔ gameabc_Layer.json ↔ gameabc_Object.json 关系
```
Layer00008.xml gameabc_Layer.json gameabc_Object.json
───────────────────── ───────────────────── ──────────────────────
<Spirit ID="145"> LayerList[8] ObjectList[144]
<Property ObjectList: [ "Property":
Name="公告内容" 3122, 145, 3121, ObjectID: 145,
ImgResID="..."/> 3017, 5, 235, ObjectType: 4,
X="81" Y="787" 604, 3057, 15] ObjectName: "公告内容",
Width="60" Height="20" Left: 81, Top: 787,
BelongGroupID="3"/> Width: 60, Height: 20,
... GroupID: 3,
Text: "公告内容",
...
}
}
gameabc_Object.json[144] 即 Object 145(数组 index = ObjectID - 1)
```
**Layer 8 → ObjectList 9 IDs → 9 个 ObjectList 索引** 完全 1:1 映射。
## 关键数据关系图
```
Project (gameabc_Project.json)
└─ ProjectName + ScreenWidth/Height + 全局事件 + fps
│
▼
Layer (gameabc_Layer.json)
└─ LayerID + LayerName + ObjectList[]
│
▼
Object (gameabc_Object.json) ← Layer ↔ Object N:1
├─ Property (41 字段) ObjectID ↔ array index
│ ├─ ObjectType (0-5) ├─ 0=? 1=? 2=Image/Sprite 3=? 4=EditBox 5=?
│ ├─ ObjectName (中文) ├─ GroupID → 包含此 object 的 group ID
│ ├─ Left/Top (绝对坐标) │
│ ├─ Width/Height ├─ Parent (父 object, 0=无)
│ ├─ GroupID (0=根) └─ OriginID (锚点参考, 0=绝对坐标)
│ ├─ ImageFileID │
│ │ │ Group (gameabc_GroupList.json)
│ │ ▼ └─ GroupID + ObjectList[] (本组所有 object)
│ └─ FrameIndex + FrameStyle
│ │
│ ├─ Text + FontSize (if EditBox/Text)
│ ├─ L9/T9/R9/B9 (9-slice border)
│ └─ offX/offY
│
├─ Event (4 字段)
│ └─ mousedown/mouseup/mousemove/ontimer → callback ObjectID
│
└─ Option (11 字段)
├─ canclick / visbale (拼写错误!)
└─ tag1-3 / vx,vy,vw,vh (动画/物理)
Image (gameabc_Image.json)
└─ id (=ImageFileID) + w_all/h_all/w/h/frame_all/bmp + w1/h1
│
▼
SpriteFrame (Cocos)
├─ name = bmp (00001.png)
├─ rect (sub-region in atlas)
├─ capInsets = [L9, T9, R9, B9]
└─ texture = atlas image asset UUID
Voice (gameabc_Voice.json) ──→ VoiceFileID → .mp3
GameTxt (gameabc_GameTxt.json) ──→ Object.GameTxtStyle → 多语言文案
```
## 与 Cocos prefab 的字段映射(迁移目标)
| 原工程字段 (gameabc_Object.Property) | Cocos prefab 字段 | 转换公式/规则 |
|---|---|---|
| ObjectID + ObjectName | `_id` (数字) + `_name` | 字符串直接 |
| ObjectType | cc.Sprite / cc.Label / cc.ProgressBar / cc.EditBox | 按映射表(待 gameabc.min.js 逆向确认完整 0-5) |
| Left, Top | `_lpos.x, _lpos.y` | `x = Left + Width/2 - 640, y = 360 - (Top + Height/2)` |
| Width, Height | `_contentSize.width, .height` | 直接 |
| GroupID | (不映射为单独字段 → 父节点 group-X) | group-X 容器 nodes + children |
| BelongLayerID | cc.Node._parent | 跨 layer 引用 |
| ImageFileID + FrameIndex | cc.SpriteFrame.__uuid__ | atlas-lookup (multi-frame rule) |
| Text | cc.Label._string | (if EditBox → cc.EditBox.string, else cc.Label.string) |
| FontSize, FontColor, BackColor, FontBold | cc.Label._fontSize, ._color | 直接 |
| L9, T9, R9, B9 | cc.SpriteFrame.capInsets | 直接数组 |
| OriginID + OriginPos + SelfPos | cc.Node anchor logic | 9=中心(待逆向公式) |
| offX/offY | (冗余字段,验证用) | — |
| FrameStyle + FrameIndex + TextFrames | cc.ProgressBar._mode, ._progress | 9-slice 多帧公式 |
| Event: mousedown/mouseup | Button._clickEvents | (cocos Button click events 待 wire) |
| Option: canclick | 决定是否加 cc.Button | — |
| Option: tag1-3 | (用户自定义数据) | (忽略) |
| Option: vx/vy/vw/vh | (动画/物理,Cocos 端 cc.Animation / Box2D 等) | (deferred to game logic plan) |
| VoiceFileID | cc.AudioSource 资源引用 | (deferred) |
| GameTxtStyle | cc.Label._string i18n lookup | (deferred to i18n sub-task) |
## 已知未映射字段(deferred sub-tasks)
| 字段 | 含义 | 状态 |
|---|---|---|
| `Option.vx/vy/vw/vh` | 动画/物理速度 | deferred to game logic migration |
| `VoiceFileID` | 音频 | deferred to audio migration |
| `GameTxtStyle` | i18n | deferred to i18n migration |
| `Event.gamestart` 等 16+ Project 级事件 | 全局事件 | deferred to game lifecycle |
| `OriginID + OriginPos + SelfPos` | 锚点计算 | gameabc.min.js 逆向待完成 |
## 后续步骤
按用户原始指令(先停下 UI 迁移,深入阅读原工程):
- ✅ 7 个 JSON schema + XML 关系 + Object schema 完成
- ✅ ObjectType 映射实测(修正之前误判:ObjectType=4 是 EditBox 不是 Label)
- ✅ Layer 8 与 gameabc_Layer.json ObjectList 1:1 验证通过
**TODO**(等用户指示):
1. 继续看 gameabc.min.js 完整 ObjectType 0-5 映射
2. 读 FrameStyle/FrameIndex/TextFrames 在多帧 sprite 下的具体公式
3. 读 OriginID + OriginPos + SelfPos 在 9-slice 布局的具体公式
4. 输出原工程数据模型完整 spec 文档(已在本文件中)
5. 给用户报告 + 等指示继续 UI 迁移