降耗②·铺开:按试点尺度精简其余 12 篇编号正文

沿用 client 02 的保守尺度(规范条款/表格/关键代码/标题/链接全保留,
只压缩冗长代码示例、重复解说、演进历史、✅/❌ 成对代码块)逐篇精简
client 01/03/04/05/06、server 01/02/03/04、engineering 01/02/03。

- 12 篇合计 78,171 → 74,726 字符(省 3,445,~4.4%);红线密集篇(client
  05、server 04)极保守、几乎不动,符合"不丢规范优先于省字数"。
- 已核验:51 个跨文档链接目标全部存在、3 个锚点全部命中真实标题;
  server 03 §6、server 04 §8/§10/§11 等被引用小节标题逐字未改;README 未动。
- 每篇均随附规范保留清单逐条自查(并行子代理完成、逐份复核)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 08:52:16 +08:00
co-authored by Claude Opus 4.8
parent 7b43d3ee44
commit f2f618b965
12 changed files with 72 additions and 213 deletions
@@ -1,6 +1,6 @@
# 01 · 前端架构与运行环境
本篇建立**全局认知**:前端跑在什么环境、由哪几层构成、新旧两套架构如何并存、文件按什么顺序加载。读懂这一篇,后面的渲染、系统、网络才有坐标。
本篇建立**全局认知**:前端跑在什么环境、由哪几层构成、新旧两套架构如何并存、文件按什么顺序加载。
> 举例以麻将为主,但 `gameabc-framework` 是游戏中立的通用框架,本篇机制对任意子游戏一致。
@@ -40,7 +40,7 @@ js/01_SubGame/ 子游戏前端
> `codes/` 内部如何分目录、如何命名文件,均由子游戏自行决定,本套文档不作规定;唯一例外是 `shared/`——它是服务端共享算法的同步副本,前端只读(见 05)。
依赖方向是**单向**的:`01_SubGame/codes` → `gameabc-framework` → `gameabc.min.js`。框架**不反向依赖**任何子游戏(这正是上一步把 `EventBus` 里的麻将事件剥离出去的原因,见 03/05 的「框架中立」)。
依赖方向是**单向**的:`01_SubGame/codes` → `gameabc-framework` → `gameabc.min.js`。框架**不反向依赖**任何子游戏(见 03/05 的「框架中立」)。
---
@@ -88,8 +88,6 @@ js/01_SubGame/ 子游戏前端
1. **被依赖者先加载**。例如框架 `EventBus.js` 必须在子游戏事件常量之前、事件常量又必须在任何注册这些事件的视图之前;整合所有精灵常量的入口文件必须**最后**加载。
2. **新增文件要插对位置**。新增一个常量/组件文件,必须在 `index.html` 里插到其依赖之后、使用者之前,否则运行时拿到 `undefined`。
> 示例:把玩法专属事件从框架剥到子游戏事件常量文件时,正是把它插在 `EventBus.js`(框架)之后、其使用者(视图组件)之前,引用零改动。
---
## 5. codes/ 内部组织
@@ -111,4 +109,3 @@ js/01_SubGame/ 子游戏前端
- 加载顺序即依赖,新增文件务必插对位置。
下一篇 [02-渲染与UI组件体系](./02-渲染与UI组件体系.md) 讲:精灵怎么画、资源常量怎么组织、UI 组件怎么写。
</content>
@@ -21,18 +21,13 @@
throw new Error('[GameEvents] EventBus 未加载;请检查 index.html 加载顺序');
}
var E = EventBus.Events;
// 通用语义事件
E.GAME_STARTED = 'game:started';
E.PLAYER_DISCARDED = 'player:discarded';
E.SCENE_CHANGED = 'ui:sceneChanged';
// 玩法专属事件
E.TILES_DEALT = 'mahjong:tilesDealt';
E.MELD_FORMED = 'mahjong:meldFormed';
E.GAME_STARTED = 'game:started'; // 通用语义事件
E.TILES_DEALT = 'mahjong:tilesDealt'; // 玩法专属事件
// ...本子游戏用到的全部事件
})();
```
> 这正是本仓库的落点:框架 `EventBus` 不预置任何事件常量、保持纯粹中立,所有事件(含通用语义)都由子游戏在其事件常量文件定义——既保证框架可被任意玩法复用,也避免「引用了未定义常量」导致的 `emit(undefined)`(见 05「框架中立」)。
> 框架 `EventBus` 不预置任何事件常量、保持纯粹中立,避免「引用了未定义常量」导致的 `emit(undefined)`(见 05「框架中立」)。
### 用法
@@ -60,7 +55,7 @@ EventBus.once(EventBus.Events.GAME_FINISHED, fn); // 一次性
3) 动画完成回调里只刷新静态界面(refresh)
```
好处:动画是“锦上添花”,**即使不播动画,静态界面也始终正确**(据组件 `this.data` 调 `refresh` 即可还原,见 02 组件范式 / 04 重连)。**动画的开始/结束/出错等生命周期回调里只刷新界面、绝不设置核心数据**;动画期间**绝不修改游戏数据**。这样动画卡住/播错、或开发前期没有动画时,数据、逻辑与界面依然正确、互不影响。
即使不播动画,静态界面也始终正确(据组件 `this.data` 调 `refresh` 即可还原,见 02 组件范式 / 04 重连)。**动画的开始/结束/出错等生命周期回调里只刷新界面、绝不设置核心数据**;动画期间**绝不修改游戏数据**。
### AnimationManager API(框架,通用)
@@ -117,12 +112,7 @@ AnimationManager.frame(spriteId, opt.startFrame, opt.endFrame, opt.duration, opt
- **`AudioManager`(框架)**:`playSound(file)`、`playVoice(baseId, sex)`、`playVoiceBySeat(seat, baseId)`、`playMusic/stopMusic`。约定女音 ID = 男音 ID + 偏移;**不含任何牌值/动作映射**。
- **音效资源常量(子游戏)**:集中定义音效/语音文件 ID(音效、男/女语音等分类)。新增音效**只改这里**。
- **游戏音频管理(子游戏)**:把“出牌/碰/某张牌”等概念映射到音效资源键,再调 `AudioManager`。**不写文件名/数字 ID**。
```js
// 子游戏音频管理:按概念播放,内部映射到资源键并按座位性别选男/女语音
// 通用音效直接走框架:AudioManager.playSound(音效资源常量.某音效)
```
- **游戏音频管理(子游戏)**:把“出牌/碰/某张牌”等概念映射到音效资源键,再调 `AudioManager`。**不写文件名/数字 ID**。通用音效直接走框架 `AudioManager.playSound(音效资源常量.某音效)`。
**声音文件手动准备**:音频文件由开发者**手动**按平台命名规范放入资源目录(文件名/存放目录/ID→文件名转换规则以平台的资源管理接口规范为准),代码只按 ID 引用、不生成文件。因此**音效资源常量每个 ID 必须注明【类型(背景音乐 / 音效 / 语音)+ 用途 + 时长 + 播放方式(循环 / 单次)】**,让人据注释就能准确准备对应音频。
@@ -181,4 +171,3 @@ Spine 骨骼资源(`json` / `atlas` / 贴图)由开发者**手动**制作并
| Spine | 概念走 Spine 动作配置,回调走 Spine 回调分发器,改资源跑脚本 | 硬编码 spineId/animName;散接回调 |
下一篇 [04-网络对接与启动编排](./04-网络对接与启动编排.md) 讲:怎么收发包、新旧架构如何对接、一局怎么启动。
</content>
@@ -19,10 +19,9 @@
### RpcHelper(自动注入平台字段)
`RpcHelper` 把每个请求自动补齐平台必需字段,业务只传业务数据:
`RpcHelper` 把每个请求自动补齐平台必需字段(`agentid / gameid / playerid / roomcode / seat ...`),业务只传业务数据:
```js
// 自动注入:agentid / gameid / playerid / roomcode / seat ...
RpcHelper.sendGameRpc(rpc, gameData, options); // route = "room"/子游戏路由
```
@@ -38,13 +37,11 @@ RpcHelper.sendGameRpc(rpc, gameData, options); // route = "room"/子游戏路
## 2. 收包:统一分发
服务端的主动推送统一格式 `{ rpc, data }`,前端有**唯一收包入口**,再按 `rpc` 分发到对应处理器。收包分发器是**纯路由表 + 分发器**:
服务端的主动推送统一格式 `{ rpc, data }`,前端有**唯一收包入口**,再按 `rpc` 分发到对应处理器。收包分发器是**纯路由表 + 分发器**(一 rpc 一处理器):
```js
// 纯路由表 + 分发器:一 rpc 一处理器
var _handlers = {
'someRpc': function (d) { return someHandler.handleSomeRpc(d); },
'anotherRpc': function (d) { return anotherHandler.handleAnother(d); },
'error': function (d) { console.error('[dispatch]', d.message); }
// ...每个 rpc 一条
};
@@ -103,7 +100,6 @@ Game_Modify.StartWar = function (_msg) {
前端 RPC 没有“同步返回”,操作结果由服务端**后续主动推送**告知。判成败的唯一权威是推送 `data` 里的 **`success`**:
```js
// 处理器统一写法
function handleXxx(data) {
if (!data || !data.success) { /* 失败处理 */ return; }
// 成功逻辑
@@ -168,4 +164,3 @@ function handleXxx(data) {
| 业务在 controllers/managers,各司其职 | 处理器交叉重造别人的能力 |
下一篇 [05-开发规范与红线](./05-开发规范与红线.md) 汇总前端所有工程纪律。
</content>
@@ -40,7 +40,7 @@
- 资源/布局 → 子游戏精灵结构/图片资源/布局常量。
- **违例信号**:框架文件里出现 `mahjong:`、牌型、吃碰杠胡等具体玩法字样(注释举例除外,且举例应尽量中立)。
> 范例:本仓库把框架 `EventBus.js` 内**所有**预定义事件常量清空(只留空容器 `EventBus.Events={}`),全部事件(通用语义 + 玩法专属)改由子游戏的事件常量文件定义;并把通用的精灵事件控制器从子游戏抽到框架 `system/SpriteEventController.js`、剥离其中的玩法标记耦合为「全局 draw 钩子」;把 `UIManager` 的全局 UI(Loading/Message/Confirm)从「主动读子游戏精灵常量」改为「由子游戏 `init(config)` 注入」。新玩法照此扩展,框架零改动。
> 范例:框架 `EventBus.js` 预定义事件常量全部清空(只留 `EventBus.Events={}`),事件改由子游戏事件常量文件定义;通用精灵事件控制器抽到框架 `system/SpriteEventController.js` 并把玩法标记耦合改为「全局 draw 钩子」;`UIManager` 的全局 UI(Loading/Message/Confirm)由子游戏 `init(config)` 注入而非主动读子游戏常量。新玩法照此扩展,框架零改动。
---
@@ -129,5 +129,4 @@
---
至此,从架构与环境(01)、渲染与组件(02)、表现系统(03)、网络与启动(04)到工程红线(05),构成一套完整的前端子游戏开发指导。回到 [README](./README.md) 查看导航与分层模型。
</content>
至此 01–05 构成一套完整的前端子游戏开发指导。回到 [README](./README.md) 查看导航与分层模型。
@@ -44,21 +44,8 @@
`00` 是平台配置项的**结构定义**文件,配置项的**集合与结构由平台/框架固定**。子游戏接入只做一件事:**把已有配置项的值改成本游戏需要的**。
✅ **允许(改值)**:
```js
Game_Config.Max.PlayerCnt = 4; // 改人数
Game_Config.Share.title = "进贤麻将"; // 改分享标题
Game_Config.Info.TextContent = ["你好","谢谢","快点","不要","加油","收到","稍等"]; // 改常用语的【值】
Game_Config.Chat.ChatLoc = [[35,517],[1100,315]/* ... */]; // 改聊天气泡坐标【值】
```
❌ **禁止(改定义 / 结构)**:
```js
Game_Config.Info.MyNewField = 1; // ✗ 新增配置项(增定义)
delete Game_Config.Voice; // ✗ 删除配置项(删定义)
Game_Config.Max = [4]; // ✗ 改变项的类型/结构
// ✗ 重命名平台已定义的字段(如把 PlayerCnt 改成 playerCount)
```
- ✅ **允许(改值)**:给已有配置项赋值,如 `Game_Config.Max.PlayerCnt = 4;`、`Game_Config.Share.title = "进贤麻将";`、改 `Game_Config.Info.TextContent`(常用语)/`Game_Config.Chat.ChatLoc`(气泡坐标)等数组元素的【值】。
- ❌ **禁止(改定义 / 结构)**:新增配置项(`Game_Config.Info.MyNewField = 1;`);删除配置项(`delete Game_Config.Voice;`);改变项的类型/结构(`Game_Config.Max = [4];`);重命名平台已定义字段(如 `PlayerCnt` → `playerCount`)。
> **为什么**:平台代码(`00_Surface/*`)按**固定字段名**读取 `Game_Config.*`。增 / 删 / 改名定义会让平台读到 `undefined` 或破坏约定,引发线上故障。**配置项的集合与结构是平台契约,只有「值」属于子游戏**。
>
@@ -75,12 +62,10 @@ Hooks 外置模式下,职责分为三层,单向向下:
00_SubGame_Config.js 平台配置项结构 + 子游戏填【配置值】
01_SubGame_modify.js 平台接口骨架,每个接口是固定【转发壳】
02_SubGame_Input.js 平台接口骨架,每个接口是固定【转发壳】
|
| 转发壳查 SubGameHooks.X,存在则委托,不存在则平台默认(B类)/空操作(A类)
v
子游戏入口层(codes/,Hooks 外置新方案核心)
SubGameHooks{} 全局对象,接口名与平台接口一一对应;子游戏只填需要的
|
| SubGameHooks.StartWar = function(_msg){ /* 委托子游戏的开局处理 */ };
v
子游戏实现层(codes/,已有)
@@ -136,12 +121,6 @@ Game_Modify.StartWar = function (_msg) {
return SubGameHooks.StartWar(_msg);
}
};
Game_Modify.Reconnect = function (_msg) {
if (window.SubGameHooks && SubGameHooks.Reconnect) {
return SubGameHooks.Reconnect(_msg);
}
};
```
### B 类 — 有平台默认返回值(无 hook 即返回默认)
@@ -156,37 +135,15 @@ Game_Modify.getMaxPlayerCount = function (roomtype) {
}
return (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
};
Game_Modify.getLeaveLimit = function (roomtype) {
if (window.SubGameHooks && SubGameHooks.getLeaveLimit) {
return SubGameHooks.getLeaveLimit(roomtype);
}
return 10;
};
gameHallImport.isInstalled = function () {
if (window.SubGameHooks && SubGameHooks.isInstalled) {
return SubGameHooks.isInstalled();
}
return 1;
};
```
`gameHallImport.*` 侧同理(如 `isInstalled` 无 hook 返回 `1`、`getLeaveLimit` 返回 `10`)。
### C 类 — 配置数据(不是 hook,留配置文件)
`Game_Config.*`(00 全部)与房型数据(`Game_Modify.Type_1/Type_2/CreateRoomData/game_config`、`Game_Modify.combat/roomDes`)属于**配置数据**而非行为接口,不进 `SubGameHooks`。
模板在 01 文件顶部设置「配置区」,由子游戏直接填值:
```js
// 01_SubGame_modify.js 顶部:配置区(子游戏填值,与转发壳接口段物理分开)
Game_Modify.combat = null; // 战绩配置,子游戏按需赋值
Game_Modify.roomDes = ''; // 房间描述
Game_Modify.Type_1 = []; // 房型一级选项
Game_Modify.Type_2 = []; // 房型二级选项
Game_Modify.CreateRoomData = []; // 创建房间数据
Game_Modify.game_config = {}; // 游戏配置
```
模板在 01 文件顶部设置「配置区」,由子游戏直接填值(与转发壳接口段物理分开):`Game_Modify.combat/roomDes/Type_1/Type_2/CreateRoomData/game_config`。
### D 类 — 模板默认 UI 渲染(可变人数)
@@ -211,17 +168,6 @@ Game_Modify.game_config = {}; // 游戏配置
目前仅 `appStart` 存在此冲突。`gameHallImport` 侧所有同名接口均加 `hall` 前缀以区分。
```js
// SubGameHooks.js 中的写法
SubGameHooks.appStart = function () {
// 对应 Game_Modify.appStart:委托子游戏的启动编排
};
SubGameHooks.hallAppStart = function () {
// 对应 gameHallImport.appStart(大厅启动):委托子游戏的大厅初始化
};
```
---
## 6. 接口覆盖要求