lawless/docs/技术文档/TDD-03-客户端架构设计.md
徐勤民 521603a899
一些检测仍在等待运行
Docs Build / build-and-deploy (push) Waiting to run
refactor(client): 删除游戏核心管理器和场景脚本
- 移除 ConfigManager 配置管理器类
- 移除 GameManager 全局单例管理器类
- 移除 NetworkManager 网络连接管理器类
- 移除 CharacterData 和 ItemData 数据模型类
- 移除 BagScene、BattleScene、LobbyScene 等场景脚本
- 移除 EncounterBubble 和 EventFeedPanel UI组件脚本
- 更新代理邀请文档中的服务器连接方式
- 更新同步状态表格中的代理任务分配信息
- 添加 MiMo 任务完成总结和审查修复记录
2026-07-03 21:34:51 +08:00

1317 行
52 KiB
Markdown

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

# TDD-03 客户端架构设计
> **文档类型**技术设计方案Technical Design Document
> **版本**v1.1
> **日期**2026-07-06
> **关联文档**TDD-02《客户端热更新技术方案》、TDD-04《数据库表结构设计》、TDD-05《API接口设计》、TDD-06《离线挂机结算系统设计》、TDD-07《反作弊与安全设计》、GDD-03《战斗系统设计》、GDD-06《经济系统设计》、GDD-23《能量体系与功法相性设计》
---
## 1. 文档信息
| 项目 | 说明 |
|------|------|
| 目标 | 为《洪荒大陆》挂机手游定义 Cocos Creator 3.x 客户端架构方案,覆盖场景管理、状态管理、网络层、文字战报渲染、UI 框架、多端适配、热更新集成与性能优化。 |
| 读者 | 客户端开发、技术负责人、QA |
| 技术栈 | Cocos Creator 3.8.x + TypeScript + protobufjs |
| 核心约束 | 无任务系统、无赛季重置、概率/机遇驱动、文字战报GDD-02 ✅1、ATB 行动条、功法加持、能量体系非体力、服务端权威TDD-07 §2 |
| 游戏时间 | 现实:游戏 = 1:3 |
---
## 2. Cocos Creator 3.x 项目架构
### 2.1 项目目录结构
```
assets/
├── bundles/ # Asset Bundle 分包(热更粒度)
│ ├── core/ # 核心 Bundle启动器、网络、状态管理、基础工具
│ │ ├── scripts/
│ │ │ ├── launcher/ # 启动场景脚本
│ │ │ ├── net/ # 网络层WebSocket/HTTP/gRPC
│ │ │ ├── state/ # 状态管理(角色缓存、离线数据)
│ │ │ ├── config/ # 配置加载器Nacos + 本地 JSON
│ │ │ └── utils/ # 工具类(加密、压缩、时间换算)
│ │ └── prefabs/ # 核心 UI 基座Loading、Toast、遮罩
│ ├── ui/ # UI Bundle各系统界面
│ │ ├── login/ # 登录/创角
│ │ ├── lobby/ # 大厅/主界面
│ │ ├── map/ # 地图/游历/副本
│ │ ├── battle/ # 战斗/战报
│ │ ├── bag/ # 背包/装备/功法
│ │ ├── social/ # 社交/组织/交易行/拍卖
│ │ ├── setting/ # 设置/账号
│ │ └── common/ # 公共弹窗、列表组件
│ ├── atlases/ # 图集 Bundle按功能分包
│ │ ├── race_portraits/ # 种族头像19 种族 × N 境界)
│ │ ├── map_tiles/ # 地图地块
│ │ ├── skill_icons/ # 技能/功法图标
│ │ └── ui_common/ # 通用 UI 元素
│ ├── audio/ # 音频 Bundle
│ │ ├── bgm/ # 背景音乐(按世界层级分)
│ │ └── sfx/ # 音效(战斗/事件/UI
│ ├── configs/ # 配置 BundleJSON/Excel 导出)
│ │ ├── race_config.json # 种族配置
│ │ ├── realm_config.json # 境界配置
│ │ ├── skill_pool.json # 技能池
│ │ ├── event_pool.json # 事件池
│ │ └── battle_text/ # 战报文案模板
│ └── i18n/ # 国际化 Bundle
│ ├── zh-CN.json
│ └── en-US.json
├── scenes/ # 场景文件
│ ├── LaunchScene.scene # 启动场景(首包,不可热更)
│ ├── LoginScene.scene # 登录场景
│ ├── LobbyScene.scene # 大厅场景
│ ├── MapScene.scene # 地图场景
│ ├── BattleScene.scene # 战斗场景
│ ├── BagScene.scene # 背包场景
│ ├── SocialScene.scene # 社交场景
│ └── SettingScene.scene # 设置场景
├── scripts/ # 首包脚本(不可热更)
│ ├── AppEntry.ts # 应用入口
│ ├── HotfixBoot.ts # 热更新引导
│ └── NativeBridge.ts # 原生桥接(支付/推送)
└── resources/ # 首包内置资源(不可热更)
├── launch/ # 启动图/图标
└── builtin/ # 引擎内置 shader/font
```
### 2.2 场景生命周期与切换策略
| 场景 | 职责 | 加载时机 | 常驻内存 | 依赖 Bundle |
|------|------|----------|----------|-------------|
| LaunchScene | 启动器:热更检查、版本校验、资源下载进度 | 首包启动 | 否(检查完毕后卸载) | core首包内置 |
| LoginScene | 登册/登录/创角/选服 | LaunchScene 完成后 | 否 | core + ui/login |
| LobbyScene | 主界面:角色状态、修炼进度、事件入口、社交入口 | 登录成功后 | **是**(主场景常驻) | core + ui/lobby + atlases |
| MapScene | 地图/游历/副本/区域事件 | 进入地图时 | 否(切换回 Lobby 时卸载) | core + ui/map + atlases/map_tiles |
| BattleScene | 战报展示/ATB 行动条/战斗结果 | 进入战斗时 | 否 | core + ui/battle + configs/battle_text |
| BagScene | 背包/装备/功法/技能/玉简 | 打开背包时 | 否 | core + ui/bag |
| SocialScene | 社交/组织/交易行/拍卖/佣兵/悬赏 | 打开社交时 | 否 | core + ui/social |
| SettingScene | 设置/账号/热更修复/客服 | 打开设置时 | 否 | core + ui/setting |
**场景切换流程**
```
LaunchScene
│ 热更检查通过
LoginScene
│ 登录成功 + 角色数据加载完毕
LobbyScene常驻
├──► MapScene压入栈,Lobby 隐藏但不卸载)
│ │ 返回
│ ▼
│ LobbyScene 恢复显示
├──► BattleScene压入栈
│ │ 战斗结束
│ ▼
│ 回到前一场景MapScene 或 LobbyScene
├──► BagScene覆盖层,不替换当前场景
├──► SocialScene覆盖层
└──► SettingScene覆盖层
```
**切换策略**
- **栈式管理**LobbyScene 为栈底常驻;MapScene/BattleScene 为栈式压入,返回时弹出。
- **覆盖层**BagScene/SocialScene/SettingScene 以全屏遮罩形式覆盖在当前场景之上,关闭时销毁。
- **预加载**:进入 LobbyScene 后,后台预加载 ui/bag、ui/social 等高频 Bundle,减少后续打开延迟。
- **内存回收**:场景弹出后,调用 `assetManager.releaseUnusedAssets()` 释放该场景独占资源。
### 2.3 资源管理方案Asset Bundle 切分)
| Bundle | 粒度 | 热更频率 | 预估大小 | 加载策略 |
|--------|------|----------|----------|----------|
| core | 首包内置 | 仅随整包更新 | ~2MB | 启动即加载 |
| ui/login | 按场景 | 低 | ~1MB | 登录前按需加载 |
| ui/lobby | 按场景 | 中 | ~3MB | 登录后加载并常驻 |
| ui/map | 按场景 | 中 | ~2MB | 进入地图时加载 |
| ui/battle | 按场景 | 中 | ~1.5MB | 进入战斗时加载 |
| ui/bag | 按场景 | 低 | ~1MB | 预加载 |
| ui/social | 按场景 | 中 | ~2MB | 预加载 |
| ui/setting | 按场景 | 低 | ~0.5MB | 按需加载 |
| atlases/* | 按功能 | 低~中 | 各 1~5MB | 预加载 + 按需 |
| audio/bgm | 按类型 | 低 | ~10MB | 按世界层级按需加载 |
| audio/sfx | 按类型 | 中 | ~5MB | 预加载高频音效 |
| configs/* | 按功能 | 高(数值热更) | 各 0.1~1MB | 启动后加载 + Nacos 运行时覆盖 |
| i18n | 按语言 | 低 | ~0.5MB | 启动时加载对应语言 |
**资源加载优先级**core > configs > ui/lobby > atlases/ui_common > 其他按需。
---
## 3. 状态管理方案
### 3.1 状态管理架构
采用 **单向数据流 + 观察者模式**,不引入第三方状态管理库,保持轻量。
```
┌──────────────────────────────────────────────────────────┐
│ Store全局状态树
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ CharacterStore│ │ InventoryStore│ │ EconomyStore │ │
│ │ - state │ │ - items │ │ - currencies │ │
│ │ - skills │ │ - equipment │ │ - market_orders │ │
│ │ - manuals │ │ │ │ │ │
│ └──────┬──────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ EventEmitter事件总线 │ │
│ │ emit('character:updated') / on('inventory:changed') │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ UI Layer │ │ UI Layer │ │ UI Layer │ │
│ │ (Observer)│ │ (Observer)│ │ (Observer)│ │
│ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────────────────────────────────────────────┘
▲ │
│ ▼
┌────────┴───────────────────────────────────────────────┐
│ Network Layer │
│ 服务端推送 ──► Store.dispatch(action) ──► UI 自动刷新 │
└────────────────────────────────────────────────────────┘
```
### 3.2 角色数据缓存层
```typescript
// 角色状态缓存结构
interface CharacterState {
// 基础信息(来自 TDD-04 characters 表)
characterId: string;
name: string;
raceId: string;
worldTier: number;
realmTier: number;
minorRealm: number;
realmStatus: 'normal' | 'tribulation_pending' | 'breakthrough_ready';
level: number;
exp: bigint;
// 六维属性GDD-02 ✅8
baseStats: {
str: number; // 力
vit: number; // 体
wis: number; // 悟
agi: number; // 速
spi: number; // 灵(巫族为"血"
luk: number; // 命
};
// 战斗属性快照
battleStats: {
hpMax: number;
atk: number;
def: number;
speed: number;
};
// 状态值
sanCurrent: number;
sanMax: number;
crimeScore: number;
heavenlyValue: number;
karmaValue: number;
reputationScore: number;
// 能量GDD-23 ✅11/✅23
energyCurrent: number;
energyMax: number;
energyRegenRate: number; // 每现实秒恢复量
// 时间戳
lastOnlineAt: Date;
dailyResetAt: Date;
// 版本号(用于乐观更新回滚判断)
serverVersion: number;
localVersion: number;
}
```
### 3.3 离线数据与在线数据同步策略
**数据分层**
| 数据类别 | 离线可用 | 同步策略 | 冲突处理 |
|----------|----------|----------|----------|
| 角色基础状态 | 是(本地快照) | 上线时全量拉取 | 服务端覆盖本地 |
| 背包/装备 | 是(本地快照) | 上线时增量同步 | 服务端覆盖本地 |
| 货币余额 | 否(仅展示上次值) | 每次操作实时查询 | 服务端权威 |
| 境界/修为 | 是(本地快照) | 上线时全量拉取 | 服务端覆盖 |
| 市场/拍卖 | 否 | 实时查询 | 服务端权威 |
| 社交关系 | 是(本地快照) | 上线时增量同步 | 服务端覆盖 |
| 配置参数Nacos | 是(本地缓存) | 登录后长轮询更新 | 服务端推送覆盖 |
**同步流程**
```
玩家上线
拉取离线结算报告TDD-06 SettlementReport
全量拉取角色状态 GET /api/v1/characters/{id}
增量同步背包/技能/功法(对比本地 serverVersion
建立 WebSocket 长连接,订阅频道
启动 Nacos 配置长轮询
数据就绪,渲染主界面
```
### 3.4 乐观更新与服务端权威回滚
对于玩家主动操作(突破、挂单、学习技能等),客户端采用**乐观更新**策略提升响应速度:
```typescript
// 乐观更新流程
async function optimisticUpdate<T>(
action: () => Promise<T>, // 服务端请求
localApply: () => void, // 本地立即应用
rollback: () => void, // 回滚函数
showToast: (msg: string) => void // 错误提示
): Promise<T | null> {
// 1. 乐观应用本地状态
localApply();
try {
// 2. 发送服务端请求
const result = await action();
// 3. 服务端确认,用服务端数据覆盖本地
Store.dispatch('SYNC_FROM_SERVER', result);
return result;
} catch (error) {
// 4. 服务端拒绝,回滚本地状态
rollback();
showToast(error.message || '操作失败,请重试');
return null;
}
}
// 使用示例:突破
async function attemptBreakthrough(targetMinorRealm: number) {
await optimisticUpdate(
() => api.realm.breakthrough(characterId, {
target_minor_realm: targetMinorRealm,
idempotency_key: generateIdempotencyKey(),
}),
() => {
// 乐观:显示突破中动画
ui.showBreakthroughAnimation();
},
() => {
// 回滚:关闭动画,恢复原状态
ui.hideBreakthroughAnimation();
Store.dispatch('RELOAD_CHARACTER');
},
(msg) => ui.showToast(msg)
);
}
```
**回滚触发条件**
| 条件 | 处理 |
|------|------|
| 服务端返回错误码(非 0 | 回滚本地状态,显示错误信息 |
| 网络超时(>10s | 回滚本地状态,提示网络异常 |
| 服务端返回的数据与本地不一致 | 以服务端数据为准覆盖本地 |
| WebSocket 推送了状态变更通知 | 触发全量刷新对应模块 |
---
## 4. 网络层设计
### 4.1 网络层架构
```
┌─────────────────────────────────────────────────────────┐
│ NetworkManager │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ WebSocketClient│ │ HttpClient │ │ GrpcWebClient │ │
│ │ (Nakama实时) │ │ (RESTful) │ │ (可选,低延迟战斗)│ │
│ └───────┬──────┘ └───────┬──────┘ └────────┬─────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ RequestQueue & RetryManager │ │
│ │ - 请求排队(防止并发冲突) │ │
│ │ - 自动重试(指数退避) │ │
│ │ - 断线重连(指数退避 + 心跳检测) │ │
│ │ - 请求超时控制 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
### 4.2 WebSocket 长连接Nakama 实时 API
```typescript
// WebSocket 客户端封装
class NakamaWsClient {
private ws: WebSocket | null = null;
private heartbeatTimer: number = 0;
private reconnectAttempts: number = 0;
private maxReconnectAttempts: number = 10;
private reconnectDelay: number = 1000; // 初始 1s,指数退避
// 连接地址
private url: string = 'wss://nakama.honghuang.example.com/ws';
// 订阅的频道列表
private subscriptions: Map<string, (data: any) => void> = new Map();
async connect(token: string): Promise<void> {
this.ws = new WebSocket(`${this.url}?token=${encodeURIComponent(token)}`);
this.ws.onopen = () => {
console.log('[WS] Connected');
this.reconnectAttempts = 0;
this.startHeartbeat();
this.resubscribeAll();
};
this.ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
this.handleMessage(msg);
};
this.ws.onclose = (event) => {
console.log('[WS] Disconnected:', event.code, event.reason);
this.stopHeartbeat();
if (event.code !== 1000) { // 非主动关闭
this.scheduleReconnect();
}
};
this.ws.onerror = (error) => {
console.error('[WS] Error:', error);
};
}
// 心跳:每 30s 发送一次
private startHeartbeat(): void {
this.heartbeatTimer = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'heartbeat' }));
}
}, 30000);
}
// 断线重连:指数退避
private scheduleReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('[WS] Max reconnect attempts reached');
EventEmitter.emit('ws:permanent_disconnect');
return;
}
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
30000 // 最大 30s
);
this.reconnectAttempts++;
console.log(`[WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => this.connect(Store.get('sessionToken')), delay);
}
// 频道订阅
subscribe(channel: string, callback: (data: any) => void): void {
this.subscriptions.set(channel, callback);
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'subscribe',
channel: channel,
}));
}
}
// 处理服务端推送消息(对齐 TDD-05 §4.2
private handleMessage(msg: any): void {
const { channel, event_type, payload } = msg;
// 分发到对应频道的回调
const callback = this.subscriptions.get(channel);
if (callback) {
callback(payload);
}
// 全局事件分发
EventEmitter.emit(`ws:${event_type}`, payload);
}
}
```
**频道订阅对照表**(对齐 TDD-05 §4.1
| 频道 | 客户端处理 | UI 刷新 |
|------|-----------|---------|
| `region:{world_tier}:{region_id}` | 区域广播 → 走马灯/系统消息 | MapScene 广播栏 |
| `events:{character_id}` | 个人事件 → 弹窗通知 | 全局 Toast + 事件面板 |
| `battle:{battle_id}` | 战斗结算 → 跳转战报 | BattleScene 自动打开 |
| `social:{character_id}` | 社交通知 → 弹窗确认 | 全局弹窗队列 |
| `org:{guild_id}` | 组织消息 → 聊天频道 | SocialScene 聊天栏 |
### 4.3 HTTP RESTful 请求封装
```typescript
// HTTP 客户端封装(对齐 TDD-05 §2.3 统一响应格式)
class HttpClient {
private baseUrl: string = 'https://api.honghuang.example.com';
private defaultTimeout: number = 10000; // 10s
async request<T>(config: RequestConfig): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${Store.get('sessionToken')}`,
'X-Client-Version': AppVersion,
'X-Trace-Id': generateUUID(),
...config.headers,
};
// 幂等键TDD-05 §5 高敏感写操作)
if (config.idempotencyKey) {
headers['Idempotency-Key'] = config.idempotencyKey;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), config.timeout || this.defaultTimeout);
try {
const response = await fetch(`${this.baseUrl}${config.url}`, {
method: config.method,
headers,
body: config.data ? JSON.stringify(config.data) : undefined,
signal: controller.signal,
});
clearTimeout(timeoutId);
// HTTP 层错误TDD-05 §2.3
if (response.status === 401) {
// Token 无效/过期 → 跳转登录
EventEmitter.emit('auth:expired');
throw new NetworkError(1002, 'Token 已过期,请重新登录');
}
if (response.status === 429) {
// 限流 → 提示玩家稍后重试
throw new NetworkError(9001, '操作过于频繁,请稍后重试');
}
const body = await response.json();
// 业务层错误
if (body.code !== 0) {
throw new NetworkError(body.code, body.message);
}
return body.data as T;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new NetworkError(-1, '请求超时,请检查网络');
}
throw error;
}
}
}
```
### 4.4 gRPC-Web可选,低延迟战斗
对于需要低延迟的战斗接口TDD-05 §2.1),可选接入 gRPC-Web
| 场景 | 协议 | 理由 |
|------|------|------|
| 发起战斗 / PVP 挑战 | gRPC-Web | 低延迟 + 二进制序列化 |
| 查询战报 | HTTP REST | 查询类,无实时性要求 |
| 突破/渡劫/天启 | gRPC-Web | 关键写操作,需幂等+低延迟 |
| 交易行/拍卖 | HTTP REST | 查询密集,缓存友好 |
| 聊天/社交 | WebSocket | 已有长连接,复用 |
> 实现优先级:先完成 HTTP REST + WebSocket 两条通道,gRPC-Web 作为后续优化项。
### 4.5 请求重试/超时/断线重连策略
| 策略 | 参数 | 说明 |
|------|------|------|
| **请求超时** | GET: 10s, POST: 15s, 战斗: 30s | 超时后回滚乐观更新 |
| **自动重试** | 最多 3 次,指数退避 1s/2s/4s | 仅对网络错误(非业务错误)重试 |
| **幂等保护** | 每次写操作生成 `idempotency_key` | 重试时复用同一 key,服务端去重TDD-05 §5 |
| **断线重连** | WebSocket 断线后指数退避 1s~30s,最多 10 次 | 重连后自动重新订阅频道 |
| **心跳检测** | 每 30s 发送 heartbeat | 连续 3 次无响应判定断线 |
| **离线模式** | 断线后切换为只读模式 | 本地缓存数据可浏览,操作提示"网络不可用" |
---
## 5. 文字战报渲染引擎
### 5.1 战报数据结构
服务端返回的战报数据结构(对齐 TDD-05 §3.3.2 和 GDD-03 §九):
```typescript
// 战报数据结构
interface BattleReport {
battleId: string;
type: 'expedition_pve' | 'dungeon_pve' | 'pvp' | 'bounty';
worldTier: number;
realmTier: number;
gameTimestamp: string;
// 参战双方
attacker: CombatantInfo;
defender: CombatantInfo;
// ATB 行动序列
rounds: RoundLog[];
// 结果
result: {
winner: 'attacker' | 'defender' | 'draw';
endCondition: 'hp_zero' | 'timeout' | 'surrender';
};
// 掉落
drops: {
currency: Array<{ currencyCode: string; amount: number }>;
items: Array<{ itemId: string; quantity: number }>;
};
// 特殊事件
specialEvents: SpecialEvent[];
// 死亡惩罚
deathPenaltyApplied: boolean;
}
// 单次行动记录
interface RoundLog {
tick: number; // ATB 行动时间单位
actor: string; // 'attacker' | 'defender'
skillId: string;
skillName: string; // 文案热更名
action: 'attack' | 'skill' | 'miss' | 'stunned' | 'dodge' | 'heal' | 'buff';
damage: number;
isCrit: boolean;
hpAfter: { attacker: number; defender: number };
energyAfter: { attacker: number; defender: number };
message: string; // 文字战报文案种族差异化,GDD-03 ✅7
}
// 参战者信息
interface CombatantInfo {
id: string;
name: string;
race: string;
realmTier: number;
level: number;
hpMax: number;
energyMax: number;
}
```
### 5.2 文字战报渲染管线
```
战报数据 JSON
BattleReportParser解析器
├── 解析 RoundLog → 生成可视化事件序列
├── 插入特殊事件(天赋触发/元素反应/天罚等)
└── 计算 ATB 行动条时间线
BattleTimeline时间线模型
├── 每个 tick 对应一帧
├── 行动条填充动画
├── 伤害数字弹出
└── 文字描述滚动
BattleUIRendererUI 渲染器)
├── ATB 行动条组件§5.3
├── 文字滚动区域ScrollView + LabelPool
├── 伤害飘字组件
├── 角色状态面板HP/能量条)
└── 结算面板
```
### 5.3 ATB 行动条客户端动画
ATB 行动条采用 **纯 CSS/Shader 实现**,不使用 Spine 动画,保持轻量:
```typescript
// ATB 行动条组件
@Component
class ATBGauge extends Component {
@property(ProgressBar)
attackerGauge: ProgressBar = null; // 攻击方行动条
@property(ProgressBar)
defenderGauge: ProgressBar = null; // 防守方行动条
@property(Node)
attackerReadyIcon: Node = null; // 攻击方就绪图标
@property(Node)
defenderReadyIcon: Node = null; // 防守方就绪图标
private timeline: BattleTimeline;
private currentTick: number = 0;
private playbackSpeed: number = 1; // 1x / 2x / 5x
// 根据战报数据预计算每帧的 ATB 值
initFromReport(report: BattleReport): void {
this.timeline = new BattleTimeline(report);
this.currentTick = 0;
}
// 逐帧播放
update(dt: number): void {
if (!this.timeline || this.timeline.isFinished()) return;
// 按播放速度推进 tick
const tickAdvance = dt * this.playbackSpeed * 60; // 60 tick/s 基准
this.currentTick += tickAdvance;
// 获取当前帧的 ATB 值
const frame = this.timeline.getFrameAtTick(this.currentTick);
if (!frame) return;
// 更新行动条进度0~100 映射到 0~1
this.attackerGauge.progress = frame.attackerATB / 100;
this.defenderGauge.progress = frame.defenderATB / 100;
// 行动条满时显示就绪图标(带缩放动画)
this.attackerReadyIcon.active = frame.attackerATB >= 100;
this.defenderReadyIcon.active = frame.defenderATB >= 100;
// 如果当前 tick 触发了行动,播放行动效果
if (frame.action) {
this.playAction(frame.action);
}
}
// 播放单次行动效果
private playAction(action: BattleAction): void {
// 1. 高亮行动者
// 2. 显示技能名Label 弹出动画)
// 3. 伤害数字飘字tween + 缓动)
// 4. 更新 HP/能量条(平滑过渡 tween
// 5. 战斗文字滚动到最新一行
}
}
```
**行动条视觉设计**
- 行动条为水平进度条,左方攻击方(蓝色),右方防守方(红色)。
- 填充过程为匀速线性动画,速度与角色 speed 属性成正比。
- 行动条满时,对应端出现"就绪"闪烁图标。
- 行动执行后,行动条归零并重新开始填充。
- 支持 1x / 2x / 5x 三种播放速度。
### 5.4 战报回放/加速/跳过
| 功能 | 实现方式 | 说明 |
|------|----------|------|
| **正常播放** | 逐帧 tick 推进,每 tick 60fps | 默认速度 |
| **2x 加速** | tickAdvance × 2 | 行动间隔缩短 |
| **5x 加速** | tickAdvance × 5 | 快速跳过 |
| **跳过** | 直接跳到最后一帧,显示结算面板 | 玩家点击"跳过"按钮 |
| **暂停** | 暂停 tick 推进 | 玩家点击暂停 |
| **回放** | 重置 currentTick = 0,重新播放 | 战斗结束后可回放 |
| **战报详情** | 点击单次行动展开详情(命中/暴击/元素克制) | 详情面板 |
---
## 6. UI 框架
### 6.1 MVC/MVVM 分层
采用 **MVVM 变体**ViewPrefab/Scene+ ViewModelComponent 脚本)+ ModelStore
```
┌───────────────┐ ┌───────────────────┐ ┌──────────────┐
│ View │ │ ViewModel │ │ Model │
│ (Prefab) │◄───►│ (Component脚本) │◄───►│ (Store) │
│ │ │ │ │ │
│ - 节点树 │ │ - 数据绑定 │ │ - 角色状态 │
│ - 动画 │ │ - 事件处理 │ │ - 背包数据 │
│ - 布局 │ │ - 业务逻辑 │ │ - 经济数据 │
│ │ │ - UI 状态管理 │ │ - 配置缓存 │
└───────────────┘ └───────────────────┘ └──────────────┘
```
**分层规范**
| 层级 | 职责 | 禁止事项 |
|------|------|----------|
| View | 节点布局、动画播放、用户输入捕获 | 不直接访问 Store,不包含业务逻辑 |
| ViewModel | 数据转换、事件分发、UI 状态管理 | 不直接操作网络层,不持有 UI 节点引用 |
| Model | 数据存储、数据变更通知 | 不包含 UI 逻辑 |
### 6.2 弹窗管理器
```typescript
// 弹窗管理器:队列 + 优先级 + 遮罩
class DialogManager {
private queue: DialogItem[] = [];
private currentDialog: DialogItem | null = null;
private maskNode: Node | null = null;
// 弹窗优先级
enum DialogPriority {
LOW = 0, // 普通提示(可被跳过)
NORMAL = 1, // 常规弹窗(背包满、邮件通知)
HIGH = 2, // 重要弹窗(战斗结算、突破结果)
CRITICAL = 3, // 紧急弹窗(断线重连、版本强更)
SYSTEM = 4, // 系统弹窗(强制退出、封号通知)
}
// 入队
enqueue(item: DialogItem): void {
// CRITICAL/SYSTEM 级别直接插入队首
if (item.priority >= DialogPriority.CRITICAL) {
this.queue.unshift(item);
} else {
this.queue.push(item);
}
this.queue.sort((a, b) => b.priority - a.priority);
this.tryShowNext();
}
// 显示下一个
private tryShowNext(): void {
if (this.currentDialog) return; // 当前有弹窗,等待关闭
if (this.queue.length === 0) return;
this.currentDialog = this.queue.shift()!;
this.showMask(this.currentDialog.priority >= DialogPriority.HIGH);
this.loadAndShow(this.currentDialog);
}
// 关闭当前弹窗
close(dialogId: string): void {
if (this.currentDialog?.id === dialogId) {
this.currentDialog = null;
this.hideMask();
this.tryShowNext();
}
}
}
```
**弹窗类型对照表**
| 弹窗类型 | 优先级 | 队列行为 | 遮罩 |
|----------|--------|----------|------|
| 离线结算面板 | HIGH | 登录后第一个弹出 | 全屏遮罩 |
| 战斗结算 | HIGH | 覆盖层弹出 | 全屏遮罩 |
| 突破/渡劫结果 | HIGH | 覆盖层弹出 | 全屏遮罩 |
| 社交通知道侣/结义请求 | NORMAL | 排队弹出 | 半透明遮罩 |
| 市场交易确认 | NORMAL | 排队弹出 | 半透明遮罩 |
| 背包满提醒 | LOW | 排队弹出 | 无遮罩 |
| Toast 提示 | LOW | 不入队,直接显示 | 无遮罩 |
| 断线重连 | CRITICAL | 插入队首 | 全屏遮罩 |
| 版本强更 | SYSTEM | 插入队首 | 全屏遮罩 |
### 6.3 列表虚拟化
背包、交易行、拍卖列表等长列表使用**对象池 + 虚拟滚动**优化:
```typescript
// 虚拟列表组件
@Component
class VirtualList extends Component {
@property(ScrollView)
scrollView: ScrollView = null;
@property(Prefab)
itemPrefab: Prefab = null;
private pool: NodePool; // 对象池
private dataItems: any[] = []; // 全量数据
private visibleItems: Map<number, Node> = new Map(); // 可见项
private itemHeight: number = 80; // 单项高度
private bufferCount: number = 5; // 上下缓冲区
// 初始化
init(data: any[], itemHeight: number): void {
this.dataItems = data;
this.itemHeight = itemHeight;
// 计算内容总高度
const totalHeight = data.length * itemHeight;
this.scrollView.content.getComponent(UITransform)!.height = totalHeight;
// 初始化对象池(预创建屏幕可见数量 + 缓冲区)
const visibleCount = Math.ceil(this.node.getComponent(UITransform)!.height / itemHeight);
this.pool = new NodePool();
for (let i = 0; i < visibleCount + this.bufferCount * 2; i++) {
const node = instantiate(this.itemPrefab);
this.pool.put(node);
}
}
// 滚动时更新可见项
private onUpdateVisibleRange(): void {
const scrollOffset = this.scrollView.getScrollOffset();
const startIndex = Math.max(0, Math.floor(scrollOffset.y / this.itemHeight) - this.bufferCount);
const endIndex = Math.min(
this.dataItems.length,
startIndex + Math.ceil(this.node.getComponent(UITransform)!.height / this.itemHeight) + this.bufferCount * 2
);
// 回收不在可见范围内的节点
for (const [index, node] of this.visibleItems) {
if (index < startIndex || index >= endIndex) {
this.pool.put(node);
this.visibleItems.delete(index);
}
}
// 创建新可见节点
for (let i = startIndex; i < endIndex; i++) {
if (!this.visibleItems.has(i)) {
const node = this.pool.get() || instantiate(this.itemPrefab);
node.setPosition(0, -i * this.itemHeight, 0);
node.getComponent('VirtualListItem')!.updateItem(this.dataItems[i], i);
this.scrollView.content.addChild(node);
this.visibleItems.set(i, node);
}
}
}
}
```
---
## 7. 适配方案
### 7.1 刘海屏/打孔屏安全区
```typescript
// 安全区适配组件
@Component
class SafeAreaAdapter extends Component {
onLoad(): void {
// 获取安全区域Cocos 3.x 内置 API
const safeArea = sys.getSafeAreaRect();
// 适配方式:将 UI 内容区域收缩到安全区内
const widget = this.node.getComponent(Widget);
if (widget) {
widget.top = sys.windowHeight - safeArea.y - safeArea.height;
widget.bottom = safeArea.y;
widget.left = safeArea.x;
widget.right = sys.windowWidth - safeArea.x - safeArea.width;
}
}
}
```
**适配层级**
| 层级 | 内容 | 安全区处理 |
|------|------|-----------|
| 背景层 | 地图/场景背景 | 延伸到全屏(忽略安全区) |
| 内容层 | 主 UI 面板 | 收缩到安全区内 |
| 顶层 | 状态栏/导航栏 | 固定在安全区顶部/底部 |
### 7.2 折叠屏/平板横竖屏
```typescript
// 屏幕方向适配管理器
class ScreenAdapter {
private currentOrientation: 'portrait' | 'landscape' = 'portrait';
// 设计分辨率
private readonly DESIGN_WIDTH = 720;
private readonly DESIGN_HEIGHT = 1280;
init(): void {
// 监听屏幕方向变化
screen.on('orientation-change', this.onOrientationChange.bind(this));
// 监听窗口大小变化(折叠屏展开/折叠)
view.on('canvas-resize', this.onCanvasResize.bind(this));
}
private onOrientationChange(orientation: screen.Orientation): void {
if (orientation === screen.Orientation.LANDSCAPE) {
this.switchToLandscape();
} else {
this.switchToPortrait();
}
}
private switchToLandscape(): void {
this.currentOrientation = 'landscape';
// 横屏布局:左侧地图/战斗,右侧状态面板
view.setDesignResolutionSize(1280, 720, ResolutionPolicy.FIXED_HEIGHT);
EventEmitter.emit('layout:landscape');
}
private switchToPortrait(): void {
this.currentOrientation = 'portrait';
// 竖屏布局:上方内容,下方操作栏
view.setDesignResolutionSize(720, 1280, ResolutionPolicy.FIXED_WIDTH);
EventEmitter.emit('layout:portrait');
}
// 折叠屏展开检测
private onCanvasResize(): void {
const ratio = screen.width / screen.height;
if (ratio > 1.5) {
// 类似平板比例,使用横屏布局
this.switchToLandscape();
} else if (ratio < 0.7) {
// 细长屏(折叠屏折叠态),使用竖屏布局
this.switchToPortrait();
}
}
}
```
**适配策略**
| 设备类型 | 比例范围 | 布局策略 |
|----------|----------|----------|
| 标准手机 | 16:9 ~ 20:9 | 竖屏布局,固定宽度适配 |
| 折叠屏(折叠) | 21:9 ~ 25:9 | 竖屏布局,内容区加长 |
| 折叠屏(展开) | 4:3 ~ 3:2 | 横屏布局或自适应双栏 |
| 平板 | 4:3 ~ 16:10 | 横屏布局,双栏显示 |
| PC 模拟器 | 16:9 ~ 21:9 | 横屏布局,键鼠映射 |
### 7.3 PC 模拟器键盘映射
```typescript
// 键盘映射配置
const KEYBOARD_MAP: Record<string, string> = {
'KeyW': 'move_up',
'KeyS': 'move_down',
'KeyA': 'move_left',
'KeyD': 'move_right',
'Space': 'confirm',
'Escape': 'back',
'KeyB': 'open_bag',
'KeyM': 'open_map',
'KeyJ': 'open_quest', // 预留(当前无任务系统)
'KeyG': 'open_guild',
'KeyT': 'open_trade',
'Digit1': 'skill_slot_1',
'Digit2': 'skill_slot_2',
'Digit3': 'skill_slot_3',
'Digit4': 'skill_slot_4',
'F1': 'help',
'F5': 'toggle_speed', // 战斗倍速切换
};
// 键盘输入管理器
class KeyboardInputManager {
init(): void {
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
}
private onKeyDown(event: EventKeyboard): void {
const action = KEYBOARD_MAP[event.code];
if (action) {
EventEmitter.emit('input:action', action);
}
}
}
```
---
## 8. 热更新客户端集成
### 8.1 接入 TDD-02 方案
客户端热更新完全遵循 TDD-02《客户端热更新技术方案》,本节定义客户端侧的集成细节。
```typescript
// 热更新引导器LaunchScene 中运行)
class HotfixBoot {
// 本地 manifest 存储键
private readonly MANIFEST_KEY = 'hotfix_manifest_';
async run(): Promise<void> {
// 1. 读取本地各 Bundle manifest 版本
const localVersions = this.loadLocalVersions();
// 2. 请求服务端版本接口TDD-02 §5.1
const versionResp = await http.get('/hotfix/version', {
params: {
major_minor: AppVersion, // 主.次版本
bundle_versions: localVersions,
device_id: getDeviceId(),
account_id: Store.get('accountId') || '',
channel: getChannel(),
}
});
// 3. 版本兼容性检查
if (isIncompatible(AppVersion, versionResp.min_compatible_version)) {
DialogManager.enqueue({
type: 'force_update',
priority: DialogPriority.SYSTEM,
message: '请前往商店下载最新版本',
actions: [
{ text: '前往商店', callback: () => openStore() },
],
});
return;
}
// 4. 下载 manifest + 差分包
for (const bundle of versionResp.bundles) {
await this.updateBundle(bundle);
}
// 5. 标记热更完成
this.saveLocalVersions(versionResp.target_versions);
}
// 单 Bundle 更新流程TDD-02 §5.1
private async updateBundle(bundle: BundleUpdateInfo): Promise<void> {
// 下载新 manifest
const newManifest = await http.get(bundle.manifest_url);
// 签名校验TDD-02 §8.1
if (!verifySignature(newManifest, BUILTIN_PUBLIC_KEY)) {
throw new Error('Manifest 签名校验失败');
}
// 比对文件列表,生成待下载列表
const localManifest = this.getLocalManifest(bundle.name);
const diffFiles = this.diffManifests(localManifest, newManifest);
if (diffFiles.length === 0) return; // 无需更新
// 并行下载差分包(支持断点续传)
const tempDir = `${bundle.name}/_temp/`;
for (const file of diffFiles) {
await this.downloadWithRetry(file, tempDir, 3);
}
// 逐文件 MD5 校验
for (const file of diffFiles) {
const md5 = await calculateMD5(`${tempDir}/${file.path}`);
if (md5 !== file.md5) {
throw new Error(`文件校验失败: ${file.path}`);
}
}
// 原子移动到正式目录
await atomicMove(tempDir, bundle.name);
// 更新本地 manifest
this.saveLocalManifest(bundle.name, newManifest);
// 如果涉及脚本变更,触发 Bundle 重载
if (newManifest.has_script_changes) {
await this.reloadBundle(bundle.name);
}
}
}
```
### 8.2 Nacos 运行时配置集成
```typescript
// Nacos 配置客户端(对齐 TDD-02 §9.2
class NacosConfigClient {
private configs: Map<string, any> = new Map();
private polling: boolean = false;
// 配置命名空间TDD-02 §9.1
private readonly NAMESPACES = [
'economy', // 经济参数
'combat', // 战斗参数
'event', // 事件参数
'map_gen', // 地图生成
'drop', // 掉落配置
];
async init(): Promise<void> {
// 1. 拉取全量配置
for (const ns of this.NAMESPACES) {
const config = await this.fetchConfig(ns);
this.configs.set(ns, config);
}
// 2. 启动长轮询(对齐 TDD-02 §9.2
this.startLongPolling();
}
// 获取配置值
get<T>(namespace: string, key: string, defaultValue: T): T {
const config = this.configs.get(namespace);
return config?.[key] ?? defaultValue;
}
// 长轮询:监听配置变更
private async startLongPolling(): Promise<void> {
this.polling = true;
while (this.polling) {
try {
const changes = await http.get('/nacos/listen', {
params: { namespaces: this.NAMESPACES.join(',') },
timeout: 30000, // 长轮询 30s
});
for (const change of changes) {
// 合法性校验
if (this.validateConfig(change.namespace, change.data)) {
this.configs.set(change.namespace, change.data);
EventEmitter.emit(`config:${change.namespace}:updated`, change.data);
}
}
} catch (error) {
// 轮询失败,等待 5s 后重试
await sleep(5000);
}
}
}
}
```
---
## 9. 性能优化
### 9.1 内存管理
| 策略 | 实现方式 | 触发时机 |
|------|----------|----------|
| **场景资源释放** | 场景切换时调用 `assetManager.releaseUnusedAssets()` | 场景弹出/切换 |
| **图集按需加载** | 种族头像/地图地块等按需加载,用完释放 | 进入对应功能模块 |
| **音频分段加载** | BGM 按世界层级分段,切换时卸载旧段加载新段 | 切换世界层级 |
| **对象池复用** | 列表项/飘字/弹窗节点使用对象池 | 全局 |
| **纹理压缩** | 移动端使用 ETC2/ASTC,PC 使用 DXT/BC7 | 构建时自动选择 |
| **大图拆分** | 超过 2048x2048 的纹理拆分为多个小图 | 美术资源规范 |
### 9.2 GC 优化
| 策略 | 说明 |
|------|------|
| **减少临时对象** | 高频调用路径update 循环、战报渲染)避免创建临时对象,使用预分配缓冲区 |
| **字符串拼接** | 使用模板字符串或 StringBuilder 模式,避免 `+` 拼接大量字符串 |
| **数组复用** | 列表数据变更时,优先清空复用而非创建新数组 |
| **TypedArray** | 数值密集型数据(战报序列、属性数组)使用 Float32Array/Int32Array |
| **手动触发 GC** | 在场景切换完成、结算面板关闭后手动触发 `cc.gc()` |
### 9.3 渲染批处理
| 优化项 | 说明 |
|--------|------|
| **动态合批** | 同一图集的相邻节点自动合批,减少 Draw Call |
| **静态合批** | 不移动的 UI 元素标记为静态合批 |
| **减少材质切换** | 相同材质的节点排列在一起,减少 GPU 状态切换 |
| **Label 优化** | 静态文本使用 Bitmap 字体;动态文本使用系统字体缓存 |
| **Mask 裁剪** | 减少 Mask 节点嵌套,每层 Mask 增加一个 Draw Call |
### 9.4 图集策略
| 图集类型 | 粒度 | 最大大小 | 说明 |
|----------|------|----------|------|
| UI 通用图集 | 按功能模块 | 2048x2048 | 按钮/图标/边框等 |
| 种族头像图集 | 按阵营3 组) | 2048x2048 | 天道/洪荒/幽冥各一组 |
| 技能图标图集 | 按功法域 | 1024x1024 | 剑/体/法/丹/器/阵等 |
| 地图地块图集 | 按世界层级 | 2048x2048 | 每层世界一组 |
| 战报 UI 图集 | 统一 | 1024x1024 | 战斗相关 UI 元素 |
**图集加载策略**
- 首包内置 ui_common 图集(通用 UI 元素)。
- 登录后预加载当前种族所属阵营的头像图集。
- 进入地图时按需加载对应世界层级的地块图集。
- 技能图标图集在打开功法/战斗界面时加载。
---
## 10. 已确认决策记录
| # | 决策 | 来源 |
|---|------|------|
| ✅C01 | 客户端架构采用 MVVM 变体ViewPrefab+ ViewModelComponent+ ModelStore | 本文确认 |
| ✅C02 | 状态管理采用单向数据流 + 观察者模式,不引入第三方库 | 本文确认 |
| ✅C03 | 场景管理采用栈式LobbyScene 常驻为栈底)+ 覆盖层BagScene 等) | 本文确认 |
| ✅C04 | Asset Bundle 按场景/功能切分core/ui/atlases/audio/configs/i18n 六大类 | TDD-02 §3.1 |
| ✅C05 | 网络层三通道WebSocket实时推送+ HTTP RESTCRUD+ gRPC-Web可选低延迟 | TDD-05 §2.1 |
| ✅C06 | WebSocket 断线重连采用指数退避1s~30s,最多 10 次 | 本文确认 |
| ✅C07 | 写操作使用幂等键idempotency_key防重复提交 | TDD-05 §5 |
| ✅C08 | 乐观更新 + 服务端权威回滚:玩家操作立即反馈,服务端拒绝时回滚 | 本文确认 |
| ✅C09 | 文字战报渲染采用逐帧 tick 推进,支持 1x/2x/5x 速度与跳过 | GDD-03 ✅1 |
| ✅C10 | ATB 行动条采用纯 CSS/Shader 动画,不使用 Spine | 本文确认 |
| ✅C11 | 弹窗管理器支持队列/优先级5 级)/遮罩,CRITICAL 级别插队 | 本文确认 |
| ✅C12 | 长列表使用对象池 + 虚拟滚动优化 | 本文确认 |
| ✅C13 | 安全区适配:背景层全屏,内容层收缩到安全区内 | 本文确认 |
| ✅C14 | 折叠屏/平板根据宽高比自动切换横竖屏布局 | 本文确认 |
| ✅C15 | PC 模拟器支持 WASD + 快捷键映射 | 本文确认 |
| ✅C16 | 热更新接入 TDD-02 方案Asset Bundle 差量更新 + Nacos 运行时配置 | TDD-02 |
| ✅C17 | Nacos 配置客户端长轮询 30s,配置变更即时生效 | TDD-02 §9.2 |
| ✅C18 | 纹理压缩:移动端 ETC2/ASTC,PC 端 DXT/BC7 | 本文确认 |
| ✅C19 | 战报数据结构与 TDD-05 §3.3.2 对齐,支持完整回放 | TDD-05 |
| ✅C20 | 离线数据同步:上线时全量拉取角色状态 + 增量同步背包/技能 | TDD-06 |
---
## 11. 验收标准
| # | 验收条目 | 测试方法 |
|---|----------|----------|
| AC-01 | 场景切换正常LoginScene → LobbyScene → MapScene → BattleScene 各场景加载/卸载无内存泄漏 | 反复切换 50 次,监控内存曲线 |
| AC-02 | Asset Bundle 按 TDD-02 manifest 正确加载,热更后新资源生效 | 修改 Bundle 资源 → 热更 → 验证新资源 |
| AC-03 | WebSocket 断线后自动重连,重连后频道订阅恢复 | 模拟断网 → 恢复 → 验证消息接收 |
| AC-04 | 乐观更新:突破操作本地立即显示结果,服务端拒绝时正确回滚 | 模拟服务端返回错误码 |
| AC-05 | 战报渲染完整战报正确逐帧播放,1x/2x/5x 速度正确,跳过直接显示结算 | 使用标准战报数据回放 |
| AC-06 | ATB 行动条动画流畅:填充速度与 speed 属性成正比,行动条满时就绪图标闪烁 | 录屏验证动画帧率 ≥ 30fps |
| AC-07 | 弹窗队列:同时触发 5 个弹窗(含 CRITICAL 级别),正确按优先级排序弹出 | 批量触发弹窗验证顺序 |
| AC-08 | 虚拟列表:背包 500 个物品滚动流畅,内存占用稳定 | 填充 500 项滚动,监控 FPS ≥ 55 |
| AC-09 | 安全区适配iPhone 15 Pro灵动岛、华为 Mate 60打孔屏UI 不被遮挡 | 多设备实机测试 |
| AC-10 | 折叠屏适配:折叠/展开时布局自动切换,无 UI 错位 | 模拟器折叠/展开测试 |
| AC-11 | PC 模拟器WASD 移动、快捷键打开背包/地图等功能正常 | 模拟器键盘测试 |
| AC-12 | Nacos 配置热更:修改 combat 参数后客户端 30s 内生效 | 修改 Nacos → 观察客户端行为变化 |
| AC-13 | 内存控制:连续游玩 2 小时,内存增长不超过初始的 30% | 长时间运行监控 |
| AC-14 | Draw Call 控制:主界面 Draw Call ≤ 50,战斗界面 ≤ 30 | 使用 Cocos Profiler 验证 |
| AC-15 | 离线数据同步:断线 10 分钟后重连,角色状态/背包/货币与服务端一致 | 断线 mock + 重连验证 |
---
## 12. 版本记录
| 版本 | 日期 | 修订内容 | 作者 |
|------|------|----------|------|
| v1.0 | 2026-07-02 | 初稿项目架构、场景管理、状态管理、网络层、文字战报渲染、UI 框架、适配方案、热更新集成、性能优化、决策记录与验收标准 | Claude Code |
| v1.1 | 2026-07-06 | 术语对齐gRPC-Web 接口表"破界"→"天启"✅122 | MiMoCode |
---
*TDD-03 v1.1 | 2026-07-06 | 术语对齐gRPC-Web 接口表"破界"→"天启"✅122*