@maoyugames/phaser-framework 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1778 @@
1
+ import { b as HttpRequestOptions, U as Unsubscribe, H as Handler, n as LogLevel, A as AdResult, r as PurchaseResult, o as LoginResult, d as IPlatform, i as IPlatformLifecycle, S as SafeArea } from './types-DtcRFbM0.js';
2
+ export { a as HttpMethod, c as HttpResponse, I as IDisposable, e as IPlatformAds, f as IPlatformAnalytics, g as IPlatformAuth, h as IPlatformDevice, j as IPlatformNet, k as IPlatformPayment, l as IPlatformSocket, m as IPlatformStorage, L as LaunchOptions, P as PlatformInfo, p as PlatformName, q as PlatformUnsupportedError, R as Result, s as SocketOptions, t as SystemInfo, V as Vec2, u as err, v as ok } from './types-DtcRFbM0.js';
3
+ import Phaser from 'phaser';
4
+
5
+ /**
6
+ * 网络层契约。框架内置三种协议:
7
+ * - http : 请求/响应,绝大多数场景够用(默认)
8
+ * - websocket : 长连接,双向实时
9
+ * - kcp : 基于 ARQ 的可靠 UDP 语义,低延迟实时对战;
10
+ * 浏览器/小游戏无原生 UDP,默认以"KCP over WebSocket(数据报)"承载,
11
+ * Capacitor 下可替换为原生 UDP 传输。
12
+ *
13
+ * 三者都通过平台层的 IPlatformNet 拿到底层传输能力,从而跨平台一致。
14
+ */
15
+
16
+ /** 协议类型 */
17
+ type NetProtocol = 'http' | 'websocket' | 'kcp';
18
+ interface HttpClientConfig {
19
+ /** 基础 URL,会与相对 path 拼接 */
20
+ baseURL?: string;
21
+ /** 默认请求头 */
22
+ headers?: Record<string, string>;
23
+ /** 默认超时(ms) */
24
+ timeout?: number;
25
+ /** 失败重试次数(幂等请求) */
26
+ retries?: number;
27
+ }
28
+ /** 请求/响应拦截器 */
29
+ interface HttpInterceptors {
30
+ request?: (options: HttpRequestOptions) => HttpRequestOptions | Promise<HttpRequestOptions>;
31
+ response?: <T>(data: T, status: number) => T | Promise<T>;
32
+ error?: (error: Error) => void;
33
+ }
34
+ interface IHttpClient {
35
+ request<T = unknown>(options: HttpRequestOptions): Promise<T>;
36
+ get<T = unknown>(url: string, options?: Partial<HttpRequestOptions>): Promise<T>;
37
+ post<T = unknown>(url: string, body?: unknown, options?: Partial<HttpRequestOptions>): Promise<T>;
38
+ put<T = unknown>(url: string, body?: unknown, options?: Partial<HttpRequestOptions>): Promise<T>;
39
+ delete<T = unknown>(url: string, options?: Partial<HttpRequestOptions>): Promise<T>;
40
+ setHeader(key: string, value: string): void;
41
+ use(interceptors: HttpInterceptors): void;
42
+ }
43
+
44
+ type SocketState = 'connecting' | 'open' | 'closing' | 'closed';
45
+ interface SocketClientConfig {
46
+ url: string;
47
+ /** 自动重连(默认开) */
48
+ autoReconnect?: boolean;
49
+ /** 重连最大次数(默认 Infinity) */
50
+ maxReconnect?: number;
51
+ /** 重连基础间隔 ms(指数退避基数,默认 1000) */
52
+ reconnectIntervalMs?: number;
53
+ /** 心跳间隔 ms(0 关闭,默认 0) */
54
+ heartbeatIntervalMs?: number;
55
+ /** 心跳包内容(string / ArrayBuffer) */
56
+ heartbeatPayload?: string | ArrayBuffer;
57
+ }
58
+ /**
59
+ * 长连接客户端统一接口。WebSocketClient 与 KcpClient 都实现它,
60
+ * 业务切换协议时只改创建处,收发逻辑零改动。
61
+ */
62
+ interface ISocketClient {
63
+ readonly state: SocketState;
64
+ connect(): Promise<void>;
65
+ send(data: string | ArrayBuffer): void;
66
+ close(): void;
67
+ onOpen(cb: () => void): Unsubscribe;
68
+ onMessage(cb: (data: string | ArrayBuffer) => void): Unsubscribe;
69
+ onError(cb: (err: Error) => void): Unsubscribe;
70
+ onClose(cb: () => void): Unsubscribe;
71
+ }
72
+ interface KcpClientConfig extends SocketClientConfig {
73
+ /** KCP conv 会话号,需与服务端约定一致 */
74
+ conv: number;
75
+ /** 是否启用 nodelay 模式(低延迟) */
76
+ nodelay?: boolean;
77
+ /** flush 间隔 ms(KCP update 频率,默认 10) */
78
+ intervalMs?: number;
79
+ /** 快速重传阈值 */
80
+ resend?: number;
81
+ /** 是否关闭拥塞控制(实时对战常关) */
82
+ nc?: boolean;
83
+ /** 发送/接收窗口 */
84
+ sndWnd?: number;
85
+ rcvWnd?: number;
86
+ }
87
+ interface INetManager {
88
+ /** 创建 HTTP 客户端 */
89
+ http(config?: HttpClientConfig): IHttpClient;
90
+ /** 创建 WebSocket 客户端 */
91
+ websocket(config: SocketClientConfig): ISocketClient;
92
+ /** 创建 KCP 客户端 */
93
+ kcp(config: KcpClientConfig): ISocketClient;
94
+ /** 框架级默认 HTTP 客户端(单例,读取全局 config.apiBaseURL) */
95
+ readonly defaultHttp: IHttpClient;
96
+ }
97
+
98
+ /**
99
+ * 底层服务契约。业务通过 App 门面访问这些服务,永远不直接 new 实现类、
100
+ * 也不直接触碰平台 SDK。服务实现统一在平台抽象之上,自动跨平台。
101
+ */
102
+
103
+ interface IEventBus {
104
+ on<T = unknown>(event: string, handler: Handler<T>): Unsubscribe;
105
+ once<T = unknown>(event: string, handler: Handler<T>): Unsubscribe;
106
+ off(event: string, handler: Handler<unknown>): void;
107
+ emit<T = unknown>(event: string, payload?: T): void;
108
+ clear(event?: string): void;
109
+ }
110
+ interface ILogger {
111
+ level: LogLevel;
112
+ debug(...args: unknown[]): void;
113
+ info(...args: unknown[]): void;
114
+ warn(...args: unknown[]): void;
115
+ error(...args: unknown[]): void;
116
+ /** 派生带标签的子 logger */
117
+ tag(label: string): ILogger;
118
+ }
119
+ /** 全局运行时配置,bootstrap 注入,业务只读 */
120
+ interface AppConfig {
121
+ /** 业务后端基础地址 */
122
+ apiBaseURL: string;
123
+ /** 设计分辨率 */
124
+ designWidth: number;
125
+ designHeight: number;
126
+ /** 是否生产环境 */
127
+ production: boolean;
128
+ /**
129
+ * 广告位默认配置(D4):业务调用 App.ads.showRewarded() 不传 id 时,
130
+ * AdsManager 回退到这里声明的默认广告位 id。
131
+ */
132
+ ads?: {
133
+ /** 激励视频默认广告位 id */
134
+ rewardedUnitId?: string;
135
+ /** 插屏默认广告位 id */
136
+ interstitialUnitId?: string;
137
+ };
138
+ /**
139
+ * 数据上报后端配置(D4):是否启用"自有后端上报"通道及其上报路径。
140
+ * 启用后 AnalyticsManager 会把 track 事件经 App.net.defaultHttp POST 到 reportPath。
141
+ */
142
+ analytics?: {
143
+ /** 是否启用自有后端上报,默认 false(仅走平台原生埋点) */
144
+ backendEnabled?: boolean;
145
+ /** 上报路径(相对 apiBaseURL),默认 '/analytics/track' */
146
+ reportPath?: string;
147
+ };
148
+ /**
149
+ * 默认语言(优先级高于平台 system.language):
150
+ * 提供时 bootstrap 用它作为 I18n 的初始 locale。
151
+ */
152
+ defaultLocale?: string;
153
+ /** i18n 配置 */
154
+ i18n?: {
155
+ /** 兜底语言,默认 'en-US' */
156
+ fallbackLocale?: string;
157
+ };
158
+ /** 各平台广告位 / 商品等业务侧映射,按需扩展 */
159
+ extra?: Record<string, unknown>;
160
+ }
161
+ interface IConfigService {
162
+ readonly value: Readonly<AppConfig>;
163
+ get<T = unknown>(key: string, fallback?: T): T;
164
+ }
165
+ interface IAudioManager {
166
+ playMusic(key: string, opts?: {
167
+ loop?: boolean;
168
+ volume?: number;
169
+ }): void;
170
+ stopMusic(): void;
171
+ playSfx(key: string, opts?: {
172
+ volume?: number;
173
+ }): void;
174
+ setMusicVolume(v: number): void;
175
+ setSfxVolume(v: number): void;
176
+ muteAll(muted: boolean): void;
177
+ }
178
+ interface IStorageManager {
179
+ get<T = unknown>(key: string, fallback?: T): T | undefined;
180
+ set<T = unknown>(key: string, value: T): void;
181
+ remove(key: string): void;
182
+ clear(): void;
183
+ }
184
+ /**
185
+ * 资源加载抽象。处理各平台差异(如微信小游戏不支持 blob、需远程包/分包)。
186
+ * 真正的纹理/音频解码交给 Phaser,这里负责"按平台正确取到字节/URL"。
187
+ */
188
+ interface IAssetLoader {
189
+ /** 取一个可被 Phaser.load 使用的 URL(小游戏侧可能是本地临时路径) */
190
+ resolveUrl(path: string): string;
191
+ loadText(path: string): Promise<string>;
192
+ loadJson<T = unknown>(path: string): Promise<T>;
193
+ loadArrayBuffer(path: string): Promise<ArrayBuffer>;
194
+ }
195
+ interface IAdsManager {
196
+ /** 是否当前平台支持广告 */
197
+ readonly available: boolean;
198
+ preloadRewarded(adUnitId?: string): Promise<void>;
199
+ /** 展示激励视频,resolve(true) 表示看完应发奖 */
200
+ showRewarded(adUnitId?: string): Promise<boolean>;
201
+ showInterstitial(adUnitId?: string): Promise<AdResult>;
202
+ }
203
+ interface IPaymentManager {
204
+ readonly available: boolean;
205
+ purchase(productId: string, extra?: Record<string, unknown>): Promise<PurchaseResult>;
206
+ restore(): Promise<PurchaseResult[]>;
207
+ }
208
+ interface IAnalyticsManager {
209
+ /** 业务自定义事件;内部按平台分发(平台原生埋点 + 自有后端) */
210
+ track(event: string, params?: Record<string, unknown>): void;
211
+ setUserId(id: string): void;
212
+ /**
213
+ * 启用自有后端上报通道(D4)。启用后,track 事件会经框架默认 HTTP 客户端
214
+ * (App.net.defaultHttp)POST 到 reportPath(默认 '/analytics/track')。
215
+ * bootstrap 据 config.analytics.backendEnabled 决定是否调用;
216
+ * 业务也可在确认后端就绪后自行调用以动态开启。
217
+ * @param opts.reportPath 自定义上报路径(相对 baseURL)
218
+ */
219
+ enableBackend(opts?: {
220
+ reportPath?: string;
221
+ }): void;
222
+ }
223
+ interface IAccountService {
224
+ /** 调用平台登录,拿到临时凭证(交业务后端换取登录态) */
225
+ login(): Promise<LoginResult>;
226
+ /**
227
+ * 获取用户公开信息(昵称 / 头像),需平台授权(D5)。
228
+ * 平台不支持或用户拒绝授权时 resolve(null),业务据此降级展示默认头像/昵称。
229
+ */
230
+ getProfile(): Promise<{
231
+ nickname: string;
232
+ avatarUrl: string;
233
+ } | null>;
234
+ }
235
+ /** 与 Phaser.Scene 解耦的场景管理门面 */
236
+ interface ISceneManager {
237
+ /** 启动/切换到指定场景 */
238
+ goto(key: string, data?: unknown): void;
239
+ /** 叠加场景(如弹窗层) */
240
+ push(key: string, data?: unknown): void;
241
+ /** 关闭叠加场景 */
242
+ pop(key: string): void;
243
+ }
244
+
245
+ /**
246
+ * 多语言(i18n)运行时。
247
+ *
248
+ * 定位:框架级、与平台/UI 解耦的纯文案查找器。不负责加载语言包文件
249
+ * (那是业务/AssetLoader 的事),只负责:
250
+ * - 注册语言包(addBundle)
251
+ * - 切换当前语言(setLocale)
252
+ * - 取文案并做占位替换(t)
253
+ * - 缺失回退:当前 locale → fallback locale → key 本身(并 warn)
254
+ *
255
+ * locale 归一:外部来源(平台 system.language、用户设置)格式五花八门
256
+ * (zh_CN / zh-Hans / en_US / EN ...),内部统一用 normalizeLocale 归一成 xx-XX,
257
+ * 保证 addBundle 与 setLocale 用同一套键命名,避免"明明加了包却查不到"。
258
+ *
259
+ * 默认语言由整合阶段注入:
260
+ * const i18n = new I18n({ fallbackLocale: 'en-US' });
261
+ * i18n.setLocale(normalizeLocale(App.platform.info.system.language));
262
+ * (本文件不直接 new App / 读平台,只提供能力,默认值由调用方注入。)
263
+ */
264
+ /** I18n 构造选项 */
265
+ interface I18nOptions {
266
+ /** 兜底语言(当前 locale 查不到时回退到它),默认 'en-US' */
267
+ fallbackLocale?: string;
268
+ /** 初始当前语言,默认与 fallbackLocale 相同 */
269
+ locale?: string;
270
+ }
271
+ declare class I18n {
272
+ /** 当前语言(已归一) */
273
+ private _locale;
274
+ /** 兜底语言(已归一) */
275
+ private readonly fallbackLocale;
276
+ /** 语言包:locale → (key → 文案) */
277
+ private readonly bundles;
278
+ constructor(opts?: I18nOptions);
279
+ /** 当前语言(归一后的 xx-XX 形态) */
280
+ get locale(): string;
281
+ /** 切换当前语言(入参自动归一) */
282
+ setLocale(locale: string): void;
283
+ /**
284
+ * 合并注册一段语言包。多次对同一 locale 调用会浅合并(后注册覆盖同名 key),
285
+ * 便于按模块分包加载(如先注册通用包,再注册某场景专属包)。
286
+ * locale 入参自动归一。
287
+ */
288
+ addBundle(locale: string, dict: Record<string, string>): void;
289
+ /**
290
+ * 取文案。
291
+ * 查找顺序:当前 locale → fallback locale → 返回 key 本身(并 warn)。
292
+ * 命中后用 params 做 {name} 占位替换。
293
+ *
294
+ * @param key 文案键
295
+ * @param params 占位参数,如 t('hello', { name: 'Tom' }) 替换 "{name}"
296
+ */
297
+ t(key: string, params?: Record<string, string | number>): string;
298
+ /** 当前 locale 或 fallback locale 中是否存在该 key */
299
+ has(key: string): boolean;
300
+ /**
301
+ * 按"当前 locale → fallback locale"顺序查找原始模板,均无则 undefined。
302
+ * 不在此 warn(由 t 决定是否 warn,has 不应产生噪声日志)。
303
+ */
304
+ private lookup;
305
+ }
306
+ /**
307
+ * 把各种来源的语言标记尽力归一成 BCP-47 风格的 'xx-XX'(语言小写、地区大写)。
308
+ *
309
+ * 归一规则(尽力而为,覆盖常见形态;非穷举,够日常宿主语言用):
310
+ * 1. 统一分隔符:下划线 '_' 视同连字符 '-'(zh_CN → zh-CN)。
311
+ * 2. 拆成 [语言, 子标签...]:语言段强制小写。
312
+ * 3. 文字系统(script,4 字母如 Hans/Hant)映射到地区:
313
+ * zh-Hans → zh-CN(简体),zh-Hant → zh-TW(繁体)。
314
+ * 这是游戏文案最常见的需求(只区分简繁),不做完整 script 保留。
315
+ * 4. 地区段(2 字母)强制大写:en-us → en-US。
316
+ * 5. 只有语言段时原样小写返回:en → en。
317
+ * 6. 空 / 非法输入回退 'en-US'。
318
+ *
319
+ * 注意:这不是完整的 BCP-47 解析器(不处理 variant / extension / 三字母语言),
320
+ * 只覆盖手游宿主常见的 zh-CN / zh-TW / en-US / ja-JP / ko-KR 等场景。
321
+ */
322
+ declare function normalizeLocale(raw: string): string;
323
+
324
+ /**
325
+ * 存档系统(SaveManager / SaveSlot)。
326
+ *
327
+ * 定位:在 App.storage(IStorageManager,已内置 JSON 序列化 + 异常兜底)之上,
328
+ * 再叠加一层"版本迁移 + 数据校验"的存档能力,解决两个真实痛点:
329
+ *
330
+ * 1. 版本迁移:游戏迭代过程中存档结构会变(新增字段、字段改名、结构调整)。
331
+ * 每个存档槽声明一个 version,旧存档读出来若版本落后,自动调用 migrate
332
+ * 升级到当前版本并写回,业务拿到的永远是最新结构。
333
+ *
334
+ * 2. 数据校验:本地存储可能被外部篡改 / 写入中途断电产生半截数据。
335
+ * 存档以 { v, data, sum } 落盘,sum 是对 data 序列化后的轻量校验和。
336
+ * load 时若校验不通过,警告并回退到 defaults,避免业务拿到脏数据崩溃。
337
+ *
338
+ * 使用方式(整合阶段在 bootstrap 处构造单例并挂到 App 上,业务再 register):
339
+ * const sm = new SaveManager();
340
+ * const slot = sm.register<PlayerData>({
341
+ * key: 'player', version: 3, defaults: { gold: 0, level: 1 },
342
+ * migrate: (old, oldV) => migratePlayer(old, oldV),
343
+ * });
344
+ * const data = slot.load();
345
+ * slot.patch({ gold: data.gold + 10 });
346
+ */
347
+ /**
348
+ * 注册一个存档槽所需的配置。
349
+ * @template T 存档数据结构(业务自定义,需可 JSON 序列化)
350
+ */
351
+ interface SaveSlotOptions<T> {
352
+ /** 存档键(会作为 App.storage 的 key,建议全局唯一,如 'player' / 'settings') */
353
+ key: string;
354
+ /** 当前存档结构版本号(从 1 开始递增,每次结构破坏性变更 +1) */
355
+ version: number;
356
+ /** 默认值:无存档 / 校验失败 / 迁移失败时回退到它(返回时深拷贝,避免外部改动污染) */
357
+ defaults: T;
358
+ /**
359
+ * 迁移函数:旧存档版本低于当前 version 时调用。
360
+ *
361
+ * 迁移策略:本实现采用"一次性迁移"——把整份旧 data 与其 oldVersion 交给 migrate,
362
+ * 由 migrate 负责一步到位升级到当前结构。之所以不做框架级"逐版本链式迁移",
363
+ * 是为了让业务对升级路径有完全掌控(可在 migrate 内部按 oldVersion 走 switch 自行逐版本处理),
364
+ * 框架不强加 1→2→3 的注册形态,保持 API 极简。
365
+ *
366
+ * @param oldData 旧版本读出的原始 data(结构未知,业务按 oldVersion 判断)
367
+ * @param oldVersion 旧存档的版本号
368
+ * @returns 升级到当前 version 的完整 data
369
+ */
370
+ migrate?: (oldData: any, oldVersion: number) => T;
371
+ }
372
+ /**
373
+ * 单个存档槽的读写句柄。
374
+ * @template T 存档数据结构
375
+ */
376
+ interface SaveSlot<T> {
377
+ /**
378
+ * 读取存档。
379
+ * - 无记录 → 返回 defaults 的深拷贝;
380
+ * - 版本落后 → 调用 migrate 升级并写回新版本后返回;
381
+ * - 校验失败 → warn 并回退 defaults 深拷贝。
382
+ */
383
+ load(): T;
384
+ /** 全量保存:写入 data 并按当前 version 落盘 { v, data, sum } */
385
+ save(data: T): void;
386
+ /** 增量更新:读出当前存档,浅合并 partial,再保存 */
387
+ patch(partial: Partial<T>): void;
388
+ /** 重置为 defaults 并写回存储 */
389
+ reset(): void;
390
+ /** 当前存档槽的版本号(只读) */
391
+ readonly version: number;
392
+ }
393
+ /**
394
+ * 存档系统门面。负责注册存档槽;真正的读写逻辑在每个 SaveSlotImpl 内。
395
+ *
396
+ * 设计为可多次 register 不同 key 的轻量管理器;同一 key 重复 register 会复用已注册的槽
397
+ * (避免同一存档被两套配置写坏),并 warn 提示。
398
+ */
399
+ declare class SaveManager {
400
+ /** 已注册的存档槽,按 key 去重 */
401
+ private readonly slots;
402
+ /**
403
+ * 注册一个存档槽。返回的 SaveSlot 即业务读写存档的唯一入口。
404
+ * @template T 存档数据结构
405
+ */
406
+ register<T>(opts: SaveSlotOptions<T>): SaveSlot<T>;
407
+ }
408
+
409
+ /**
410
+ * 单张配置表(ConfigTable)。
411
+ *
412
+ * 数据驱动:策划导出的数据(数组或字典)经主键索引后,业务按 id 读取。
413
+ * 表内数据视为只读,业务不应修改返回的对象引用。
414
+ */
415
+ declare class ConfigTable<T> {
416
+ /** 主键(string|number 统一转 string 作为 Map key) -> 行数据 */
417
+ private readonly index;
418
+ /**
419
+ * @param rows 已构造好的行集合
420
+ * @param primaryKey 主键字段名(默认 'id'),用于建立索引
421
+ */
422
+ constructor(rows: T[], primaryKey?: string);
423
+ /** 按主键取一行;不存在返回 undefined */
424
+ get(id: string | number): T | undefined;
425
+ /** 返回全部行(顺序为插入顺序) */
426
+ all(): T[];
427
+ /** 是否存在某主键 */
428
+ has(id: string | number): boolean;
429
+ /** 行数 */
430
+ get size(): number;
431
+ }
432
+
433
+ /**
434
+ * 配置表管理器(ConfigTableManager)。
435
+ *
436
+ * 职责:按名加载 JSON 配置数据、建表并缓存,供全局按名复用。
437
+ *
438
+ * - 数据来源统一走 App.assets.loadJson(跨平台正确取字节/URL)。
439
+ * - 支持两种 JSON 形态:
440
+ * 数组形态 [{ id, ... }, ...]
441
+ * 字典形态 { "1001": { ... }, "1002": { ... } }
442
+ * 字典形态会把每个值取出组成行;若值自身缺主键字段,则用字典的 key 回填主键。
443
+ * - 同名表重复 load 直接复用缓存,不重复请求(除非先 unload)。
444
+ */
445
+
446
+ declare class ConfigTableManager {
447
+ /** 表名 -> 已建好的配置表(用 unknown 擦除元素类型,get<T> 时再断言) */
448
+ private readonly tables;
449
+ /**
450
+ * 按名加载并缓存一张配置表。已缓存则直接返回缓存,不重复请求。
451
+ *
452
+ * @param name 表名(后续 get/has/unload 的键)
453
+ * @param path 资源路径(交给 App.assets.loadJson)
454
+ * @param primaryKey 主键字段名,默认 'id'
455
+ */
456
+ load<T>(name: string, path: string, primaryKey?: string): Promise<ConfigTable<T>>;
457
+ /**
458
+ * 取已加载的表;未加载抛出清晰错误(提示先 load)。
459
+ */
460
+ get<T>(name: string): ConfigTable<T>;
461
+ /** 是否已加载某表 */
462
+ has(name: string): boolean;
463
+ /** 卸载某表缓存(下次 load 会重新请求);返回是否确有该表 */
464
+ unload(name: string): boolean;
465
+ /**
466
+ * 把数组或字典两种形态归一化成行数组。
467
+ * - 数组:直接使用。
468
+ * - 字典:取所有值;若某值缺主键,则用字典 key 回填(便于字典形态省略 id)。
469
+ */
470
+ private normalize;
471
+ }
472
+
473
+ /**
474
+ * 面板管理器 PanelManager:统一管理"弹出式面板"的栈与层级。
475
+ *
476
+ * 设计目标:让业务只关心"开/关哪个面板",不关心加到哪个场景、设多少 depth、谁压谁。
477
+ * - 面板按"注册 key → 构造器"登记,open(key) 时按需实例化。
478
+ * - 面板加入"当前活动场景"的显示列表(取 game.scene.getScenes(true) 最上层那个)。
479
+ * - depth 用 `Layers.Panel + 栈深` 递增,保证后开的面板压在先开的之上。
480
+ * - 维护一个面板栈:closeTop / closeAll 配合返回键 / 全局关闭等常见交互。
481
+ *
482
+ * 与 bootstrap 的关系:整合阶段 bootstrap 创建 Phaser.Game 后调用 bindGame(game),
483
+ * 之后业务即可通过 App 门面拿到的 PanelManager 实例随处开关面板。
484
+ */
485
+
486
+ /** 面板构造器签名:接收所属场景 */
487
+ type PanelCtor = new (scene: Phaser.Scene) => BasePanel;
488
+ /**
489
+ * 面板管理器对外契约,供 App 门面引用(业务面对接口而非实现)。
490
+ */
491
+ interface IPanelManager {
492
+ /** 注册面板:key 唯一,ctor 为面板类构造器 */
493
+ register(key: string, ctor: PanelCtor): void;
494
+ /** 打开面板(已开则返回已有实例);异步,等 onCreate 完成后 resolve */
495
+ open(key: string, data?: unknown): Promise<BasePanel>;
496
+ /** 关闭指定 key 的面板 */
497
+ close(key: string): void;
498
+ /** 关闭栈顶面板 */
499
+ closeTop(): void;
500
+ /** 关闭全部面板 */
501
+ closeAll(): void;
502
+ /** 指定 key 的面板是否打开中 */
503
+ isOpen(key: string): boolean;
504
+ }
505
+ declare class PanelManager implements IPanelManager {
506
+ /** key → 构造器 */
507
+ private readonly registry;
508
+ /** 打开中的面板栈(后进者在数组尾,即栈顶) */
509
+ private readonly stack;
510
+ /** bootstrap 注入的 Phaser.Game,用于取当前活动场景 */
511
+ private game;
512
+ /** 由 bootstrap 在 Phaser.Game 创建后调用 */
513
+ bindGame(game: Phaser.Game): void;
514
+ register(key: string, ctor: PanelCtor): void;
515
+ /**
516
+ * 打开面板。流程:
517
+ * 取栈顶活动场景 → new 面板 → 加入场景显示列表 → setDepth(Layers.Panel + 栈深)
518
+ * → await onCreate → onShow → 压栈
519
+ * 若同 key 已打开,直接返回已有实例(不重复创建)。
520
+ */
521
+ open(key: string, data?: unknown): Promise<BasePanel>;
522
+ close(key: string): void;
523
+ closeTop(): void;
524
+ closeAll(): void;
525
+ isOpen(key: string): boolean;
526
+ /** 当前打开的面板数量 */
527
+ get stackSize(): number;
528
+ /** 执行一个面板的 onHide → onClose(释放) */
529
+ private teardown;
530
+ /** 关闭非栈顶面板后,按当前栈顺序重排剩余面板的 depth */
531
+ private reindexDepth;
532
+ private find;
533
+ /**
534
+ * 取"当前活动场景"作为面板宿主:用 game.scene.getScenes(true)(运行中的场景)
535
+ * 的最上层(数组末尾,渲染序最高)那个。
536
+ *
537
+ * 扩展点:若业务需要把面板固定挂到某个专用 UI 场景(而非当前场景),
538
+ * 可在此处改为按指定 sceneKey 取场景,或给 open 增加 targetScene 参数后在此分发。
539
+ */
540
+ private activeScene;
541
+ }
542
+
543
+ /**
544
+ * 面板基类 BasePanel(基于 Phaser.GameObjects.Container)。
545
+ *
546
+ * 业务写一个面板(商店、背包、设置、对话框……)的标准姿势:
547
+ * export class ShopPanel extends BasePanel {
548
+ * protected async onCreate(data?: unknown) {
549
+ * this.addDim(); // 半透明遮罩(可选)
550
+ * const bg = this.scene.make.image({ ... }, false);
551
+ * this.add(bg); // 子对象 add 进容器,不重复加入场景根
552
+ * const close = new Button(this.scene, { text: '关闭', onClick: () => this.close() });
553
+ * this.add(close);
554
+ * this.bind(this.app.events.on('coin:change', (n) => { ... })); // 订阅,关闭时自动清理
555
+ * }
556
+ * protected onShow() { ... } // 入场动画 / 刷新数据
557
+ * protected onHide() { ... } // 退场动画
558
+ * // onClose 一般不需覆写:基类默认会清理所有 bind 并 destroy 容器
559
+ * }
560
+ *
561
+ * 设计要点(为减少 AI 漏清理导致的泄漏):
562
+ * - 生命周期清晰:onCreate(构建)→ onShow(显示)→ onHide(隐藏)→ onClose(释放)。
563
+ * - 清理自动化:任何需要在关闭时撤销的订阅/句柄,用 `this.bind(unsub)` 登记;
564
+ * 基类 onClose 默认实现会统一执行全部清理并销毁容器,业务无需手写 destroy。
565
+ * - 面板自身不直接出现在 PanelManager 之外的代码里管理生命周期;关闭走 `this.close()`。
566
+ */
567
+
568
+ declare abstract class BasePanel extends Phaser.GameObjects.Container {
569
+ /** 打开本面板时所属的 key(由 PanelManager 在创建后注入,用于自我关闭) */
570
+ private _key;
571
+ /** 管理本面板的 PanelManager(由其在创建后注入) */
572
+ private _manager;
573
+ /** open 时透传进来的业务数据 */
574
+ private _data;
575
+ /** 关闭时需要逐一执行的清理句柄(订阅、定时器等) */
576
+ private readonly disposers;
577
+ /** 防止重复关闭 */
578
+ private closing;
579
+ constructor(scene: Phaser.Scene);
580
+ /** App 门面:面板内访问网络/音频/存储/事件等的统一入口 */
581
+ protected get app(): typeof App;
582
+ /**
583
+ * open 时传入的业务数据(子类在 onCreate/onShow 中读取)。
584
+ * 注意:Phaser.Container 自身的 `data` 字段是其内置 DataManager,
585
+ * 故业务数据用 `panelData` 暴露,避免与基类字段冲突。
586
+ */
587
+ protected get panelData(): unknown;
588
+ /** 本面板的注册 key */
589
+ get key(): string;
590
+ /**
591
+ * 构建 UI。子类覆写,在此创建并 add 所有子对象。
592
+ * 可返回 Promise(异步加载资源/数据时),PanelManager 会 await 后再 onShow。
593
+ * @param data open 时透传的业务数据(等价于 this.data)
594
+ */
595
+ protected onCreate(_data?: unknown): void | Promise<void>;
596
+ /** 显示时回调(入场动画 / 数据刷新)。子类按需覆写。 */
597
+ protected onShow(): void;
598
+ /** 隐藏时回调(退场动画)。子类按需覆写。在 onClose 之前调用。 */
599
+ protected onHide(): void;
600
+ /**
601
+ * 释放回调。默认实现:执行全部 bind 登记的清理 + 销毁容器。
602
+ * 子类如需自定义释放逻辑,覆写后请调用 super.onClose() 以保证基础清理执行。
603
+ */
604
+ protected onClose(): void;
605
+ /**
606
+ * 登记一个需在面板关闭时自动撤销的句柄(事件订阅返回的 Unsubscribe、
607
+ * 自定义清理函数等)。基类 onClose 会统一执行,业务无需手动记账。
608
+ */
609
+ protected bind(unsub: Unsubscribe): void;
610
+ /** 关闭自己(委托给 PanelManager 走完整的 onHide → onClose → 出栈)。 */
611
+ close(): void;
612
+ /**
613
+ * 添加一层覆盖全屏的半透明遮罩(放在面板最底层),点击可选触发关闭。
614
+ * 以设计稿尺寸铺满;遮罩本身加入容器,随面板一起销毁。
615
+ * @param opts.color 遮罩颜色(默认黑)
616
+ * @param opts.alpha 不透明度(默认 0.6)
617
+ * @param opts.closeOnClick 点击遮罩是否关闭面板(默认 false)
618
+ */
619
+ protected addDim(opts?: {
620
+ color?: number;
621
+ alpha?: number;
622
+ closeOnClick?: boolean;
623
+ }): Phaser.GameObjects.Graphics;
624
+ /**
625
+ * 把一个子对象放到设计稿正中(以设计稿坐标系)。
626
+ * 仅设置坐标,不负责 add(由调用方决定何时 add 进容器)。
627
+ */
628
+ protected centerChild(child: Phaser.GameObjects.Components.Transform): void;
629
+ /** 设计稿宽度(来自 App.config) */
630
+ protected get designWidth(): number;
631
+ /** 设计稿高度(来自 App.config) */
632
+ protected get designHeight(): number;
633
+ /**
634
+ * PanelManager 创建面板后注入归属信息。下划线前缀表示"框架内部使用",业务勿调。
635
+ */
636
+ __attach(manager: IPanelManager, key: string, data: unknown): void;
637
+ /** 触发 onCreate(可能异步)。仅 PanelManager 调用。 */
638
+ __create(): void | Promise<void>;
639
+ /** 触发 onShow。仅 PanelManager 调用。 */
640
+ __show(): void;
641
+ /** 触发 onHide。仅 PanelManager 调用。 */
642
+ __hide(): void;
643
+ /** 触发 onClose(释放)。仅 PanelManager 调用,带防重入。 */
644
+ __close(): void;
645
+ /** 逐一执行并清空已登记的清理句柄(单个失败不影响其余)。 */
646
+ private runDisposers;
647
+ /** 容器是否已被销毁(Phaser 销毁后 scene 引用会被清空) */
648
+ private get destroyed();
649
+ }
650
+
651
+ /**
652
+ * 全局层级(depth)常量。
653
+ *
654
+ * Phaser 用 `gameObject.setDepth(n)` 决定同一显示列表内的渲染先后:depth 大者后绘、显示在上层。
655
+ * 框架把"全屏 UI 类对象"划分为若干固定层段,业务统一引用本表,避免各处 setDepth 魔法数字打架。
656
+ *
657
+ * setDepth 约定:
658
+ * - 同一层段内可用 `Layers.X + 局部偏移` 微调先后(如 Panel 栈深),但不要跨段污染。
659
+ * - PanelManager 开面板时按 `Layers.Panel + 栈深` 设置,保证后开的面板压在先开的之上。
660
+ * - 业务自己的 HUD / Toast / Loading / Guide 也应取对应层段,确保它们恒在面板之上或之下的预期位置。
661
+ *
662
+ * 各层用途:
663
+ * - Scene 场景背景层(地图、天空盒、场景静态装饰)。
664
+ * - Entity 实体层(角色、怪物、道具、子弹等可玩对象)。
665
+ * - Hud 常驻 HUD(血条、分数、摇杆、顶栏),恒在实体之上、面板之下。
666
+ * - Panel 弹出式面板(商店、背包、设置、对话框),由 PanelManager 管理的栈。
667
+ * - Toast 轻提示(飘字、Toast、气泡),压在面板之上但不抢占输入焦点。
668
+ * - Loading 加载遮罩 / 转场遮罩,几乎盖住一切。
669
+ * - Guide 新手引导高亮 / 蒙层,需要在最顶层强制吸引注意。
670
+ */
671
+ declare const Layers: {
672
+ /** 场景背景层 */
673
+ readonly Scene: 0;
674
+ /** 实体层(角色、道具、子弹等) */
675
+ readonly Entity: 100;
676
+ /** 常驻 HUD 层 */
677
+ readonly Hud: 1000;
678
+ /** 弹出面板层(PanelManager 管理) */
679
+ readonly Panel: 2000;
680
+ /** 轻提示 / Toast 层 */
681
+ readonly Toast: 3000;
682
+ /** 加载 / 转场遮罩层 */
683
+ readonly Loading: 4000;
684
+ /** 新手引导层(最顶) */
685
+ readonly Guide: 5000;
686
+ };
687
+ /** 层级键名联合类型(便于业务做类型约束) */
688
+ type LayerName = keyof typeof Layers;
689
+
690
+ /**
691
+ * 业务模块基类 BaseModule。
692
+ *
693
+ * "模块"是业务层的一块自治功能单元(如金币模块、任务模块、好友模块):
694
+ * 持有该功能的状态与逻辑,对外通过事件总线 / 公开方法协作,不直接耦合 UI/场景。
695
+ *
696
+ * 业务写一个模块的标准姿势:
697
+ * export class CoinModule extends BaseModule {
698
+ * readonly name = 'coin';
699
+ * private coin = 0;
700
+ * async init() { // 启动时初始化(可异步:读存档/拉服务端)
701
+ * this.coin = this.app.storage.get('coin', 0)!;
702
+ * }
703
+ * add(n: number) {
704
+ * this.coin += n;
705
+ * this.app.storage.set('coin', this.coin);
706
+ * this.app.events.emit('coin:change', this.coin);
707
+ * }
708
+ * dispose() { ... } // 释放(清订阅/定时器)
709
+ * }
710
+ *
711
+ * 然后注册:registry.register(new CoinModule()); registry.initAll();
712
+ * 别处取用:registry.get<CoinModule>('coin').add(10);
713
+ *
714
+ * 设计要点:
715
+ * - name 唯一,作为 ModuleRegistry 的检索键。
716
+ * - 生命周期清晰:init(启动,可异步)→ dispose(释放)。
717
+ * - 通过 this.app 访问底层服务,模块之间不互相 new,而是经 registry.get 取依赖。
718
+ */
719
+
720
+ declare abstract class BaseModule {
721
+ /** 模块唯一名,作为 ModuleRegistry 的检索键。子类必须提供。 */
722
+ abstract readonly name: string;
723
+ /** App 门面:模块内访问网络/存储/事件等的统一入口 */
724
+ protected get app(): typeof App;
725
+ /**
726
+ * 初始化。ModuleRegistry.initAll 按注册顺序调用,可返回 Promise 以等待
727
+ * 异步初始化(读存档、拉服务端配置等)完成后再初始化下一个模块。
728
+ * 默认空实现,子类按需覆写。
729
+ */
730
+ init(): void | Promise<void>;
731
+ /**
732
+ * 释放。ModuleRegistry.disposeAll 调用,清理订阅 / 定时器 / 缓存等。
733
+ * 默认空实现,子类按需覆写。
734
+ */
735
+ dispose(): void;
736
+ }
737
+
738
+ /**
739
+ * 模块注册表 ModuleRegistry:业务模块的容器与生命周期协调者。
740
+ *
741
+ * 职责:
742
+ * - register:登记模块(按 name 唯一)。
743
+ * - initAll:按"注册顺序"依次 init,await 异步初始化,保证依赖在前者先就绪。
744
+ * - disposeAll:释放全部模块(逆注册顺序,先释放后注册的,降低相互依赖踩坑)。
745
+ * - get / has:按 name 取模块,供模块间协作或 UI/场景获取业务能力。
746
+ *
747
+ * 典型接线(整合阶段或业务启动处):
748
+ * const registry = new ModuleRegistry();
749
+ * registry.register(new CoinModule());
750
+ * registry.register(new TaskModule()); // 若 Task 依赖 Coin,Coin 先注册即先 init
751
+ * await registry.initAll();
752
+ * ...
753
+ * registry.get<CoinModule>('coin').add(10);
754
+ */
755
+
756
+ declare class ModuleRegistry {
757
+ /** name → 模块实例 */
758
+ private readonly modules;
759
+ /** 注册顺序(initAll 正序、disposeAll 逆序据此遍历) */
760
+ private readonly order;
761
+ /**
762
+ * 注册一个模块。name 重复将抛错(避免静默覆盖导致取到错误实例)。
763
+ */
764
+ register(module: BaseModule): void;
765
+ /**
766
+ * 按注册顺序依次初始化所有模块,await 异步 init。
767
+ * 串行(非并行)以尊重模块间的初始化依赖顺序。
768
+ */
769
+ initAll(): Promise<void>;
770
+ /**
771
+ * 释放所有模块。按逆注册顺序释放(后注册者先释放),
772
+ * 单个模块 dispose 抛错不阻断其余模块释放。
773
+ */
774
+ disposeAll(): void;
775
+ /**
776
+ * 按 name 取模块,取不到抛清晰错误(避免业务拿到 undefined 后在更深处崩溃)。
777
+ * 泛型 T 便于直接拿到具体模块类型,无需手动断言。
778
+ */
779
+ get<T extends BaseModule = BaseModule>(name: string): T;
780
+ /** 指定 name 的模块是否已注册 */
781
+ has(name: string): boolean;
782
+ /** 已注册模块数量 */
783
+ get size(): number;
784
+ }
785
+
786
+ /**
787
+ * App 门面契约。业务层唯一的"上帝对象":
788
+ * import { App } from '@maoyugames/phaser-framework';
789
+ * App.net / App.audio / App.scenes ...
790
+ *
791
+ * 由 bootstrap 在平台初始化完成后组装出具体实例,业务全程只用接口。
792
+ */
793
+
794
+ interface IApp {
795
+ /** 当前平台(只读能力查询;具体调用优先用上层服务) */
796
+ readonly platform: IPlatform;
797
+ /** 网络:http() / websocket() / kcp() / defaultHttp */
798
+ readonly net: INetManager;
799
+ /** 全局事件总线 */
800
+ readonly events: IEventBus;
801
+ /** 日志 */
802
+ readonly log: ILogger;
803
+ /** 运行时配置(只读) */
804
+ readonly config: IConfigService;
805
+ /** 音频 */
806
+ readonly audio: IAudioManager;
807
+ /** 本地存储(带序列化) */
808
+ readonly storage: IStorageManager;
809
+ /** 资源加载(跨平台) */
810
+ readonly assets: IAssetLoader;
811
+ /** 广告 */
812
+ readonly ads: IAdsManager;
813
+ /** 支付/内购 */
814
+ readonly payment: IPaymentManager;
815
+ /** 数据上报 */
816
+ readonly analytics: IAnalyticsManager;
817
+ /** 账号/登录 */
818
+ readonly account: IAccountService;
819
+ /** 场景管理 */
820
+ readonly scenes: ISceneManager;
821
+ /** 多语言:t() 取文案 / setLocale 切换语言 */
822
+ readonly i18n: I18n;
823
+ /** 存档系统:register 存档槽,做版本迁移 + 校验 */
824
+ readonly save: SaveManager;
825
+ /** 配置表:按名 load / get 数据驱动配置 */
826
+ readonly tables: ConfigTableManager;
827
+ /** 弹出式面板栈:open/close 面板(业务面对接口,不触碰实现) */
828
+ readonly panels: IPanelManager;
829
+ /** 业务模块注册表:register/initAll/get 业务功能模块 */
830
+ readonly modules: ModuleRegistry;
831
+ /**
832
+ * 平台前后台生命周期(经门面暴露):onShow / onHide。
833
+ * 业务直接用 App.lifecycle 订阅,不再绕 App.platform.lifecycle。
834
+ */
835
+ readonly lifecycle: IPlatformLifecycle;
836
+ }
837
+
838
+ /**
839
+ * App 门面单例。业务层唯一的"上帝对象":
840
+ * import { App } from '@maoyugames/phaser-framework';
841
+ * App.net / App.audio / App.scenes ...
842
+ *
843
+ * 延迟注入模式:
844
+ * - 模块加载时 App 各字段尚未就绪(底层服务需等平台 init 完成才能构造)。
845
+ * - bootstrap 在平台初始化、各服务构造完成后,调用内部的 __initApp(parts) 一次性注入。
846
+ * - 注入前访问任意字段都会抛出清晰错误,提示"请通过 startGame() 启动",
847
+ * 而不是让业务拿到 undefined 后在更深处莫名其妙地崩溃。
848
+ *
849
+ * 实现手法:用闭包持有可空的 parts,导出的 App 是一个 getter 代理对象,
850
+ * 每个字段 getter 在未注入时抛错,已注入时返回真实服务实例。
851
+ */
852
+
853
+ /**
854
+ * App 门面单例。所有字段为只读 getter,实现延迟注入。
855
+ * 类型严格实现 IApp,业务面对的就是接口,无法触碰底层实现细节。
856
+ */
857
+ declare const App: IApp;
858
+
859
+ /**
860
+ * 框架启动流程。平台入口(main.<platform>.ts)调用 startGame() 拉起整个游戏:
861
+ *
862
+ * import { startGame } from '@maoyugames/phaser-framework';
863
+ * import { WebPlatform } from '@maoyugames/phaser-framework/platform/web';
864
+ * startGame({ platform: new WebPlatform(), config: gameConfig, scenes });
865
+ *
866
+ * 启动顺序:
867
+ * ① PlatformContext.set(platform) 注入当前平台实现(全局唯一)
868
+ * ② await platform.init() 平台初始化(读系统信息、注入 SDK、起适配器)
869
+ * ③ 构造各底层服务 NetManager / EventBus / Logger / Config / Storage /
870
+ * Audio / Assets / Ads / Payment / Analytics / Account / Scene
871
+ * + I18n / SaveManager / ConfigTableManager / ModuleRegistry / PanelManager
872
+ * (lifecycle 直接复用 platform.lifecycle)
873
+ * ④ __initApp(parts) 组装进 App 门面(此后业务可安全访问 App.*)
874
+ * ⑤ new Phaser.Game(...) 创建 Phaser 实例(FIT + CENTER_BOTH,设计分辨率来自 config)
875
+ * ⑥ 把 game 交给 AudioManager / SceneManager / PanelManager(它们依赖 game.sound / game.scene)
876
+ * ⑦ await App.modules.initAll() 初始化业务注册的模块(业务在 startGame 前/中注册;无模块也安全)
877
+ *
878
+ * 跨平台说明:
879
+ * - DOM 平台(web/tiktok/capacitor):Phaser 自动创建 canvas 挂到 document.body。
880
+ * - 微信小游戏(hasDOM=false):由 weapp-adapter 提供 document.createElement('canvas')
881
+ * 及 window 等 shim,因此这里无需特判平台,Phaser 配置一致即可。
882
+ */
883
+
884
+ /** startGame 入参 */
885
+ interface StartGameOptions {
886
+ /** 当前平台实现(由平台入口静态 import 并 new) */
887
+ platform: IPlatform;
888
+ /** 全局运行时配置 */
889
+ config: AppConfig;
890
+ /** 业务场景列表(第一个为初始场景) */
891
+ scenes: Phaser.Types.Scenes.SceneType[];
892
+ /**
893
+ * 业务注册入口(可选)。在 App 门面就绪后、创建 Phaser 场景与初始化模块之前调用。
894
+ * 业务在此统一注册:模块(App.modules.register)、面板(App.panels.register)、
895
+ * 语言包(App.i18n.addBundle)、存档槽(App.save.register)、预加载配置表(App.tables.load)等。
896
+ * 支持 async(如需 await 配置表加载)。
897
+ */
898
+ setup?: (app: IApp) => void | Promise<void>;
899
+ }
900
+ /**
901
+ * 启动游戏。平台无关,可被任意平台入口复用。
902
+ * 返回的 Promise 在 Phaser.Game READY 后 resolve。
903
+ */
904
+ declare function startGame(opts: StartGameOptions): Promise<void>;
905
+
906
+ /**
907
+ * 调度器(Scheduler)。
908
+ *
909
+ * 统一封装延迟(delay)、周期(interval)、下一帧(nextTick)三类定时,
910
+ * 关键价值在于:所有句柄被集中跟踪,可一键 clearAll——
911
+ * 让场景/面板在销毁时把自己启动的所有定时器一次性清掉,杜绝泄漏与"幽灵回调"。
912
+ *
913
+ * 实现:底层用 setTimeout/setInterval;nextTick 优先用 microtask(Promise),
914
+ * 不可用时降级 setTimeout(0)。每个句柄 cancel 幂等(重复 cancel 无副作用)。
915
+ */
916
+ interface ScheduleHandle {
917
+ /** 取消本次调度;幂等,可安全多次调用 */
918
+ cancel(): void;
919
+ }
920
+ declare class Scheduler {
921
+ /** 所有未结束的句柄;clearAll 时统一取消 */
922
+ private readonly handles;
923
+ /** 延迟 ms 后执行一次 fn */
924
+ delay(ms: number, fn: () => void): ScheduleHandle;
925
+ /** 每隔 ms 周期执行 fn,直到 cancel/clearAll */
926
+ interval(ms: number, fn: () => void): ScheduleHandle;
927
+ /** 下一个微任务(或 setTimeout 0)执行一次 fn */
928
+ nextTick(fn: () => void): ScheduleHandle;
929
+ /** 取消单个句柄(等价于 handle.cancel(),幂等) */
930
+ clear(handle: ScheduleHandle): void;
931
+ /** 取消并清空所有句柄 */
932
+ clearAll(): void;
933
+ }
934
+
935
+ /**
936
+ * 业务场景基类 BaseScene。
937
+ *
938
+ * 所有业务场景继承它(而非直接继承 Phaser.Scene),从而:
939
+ * - 通过 this.app 便捷访问 App 门面(等价于 import { App } from '@maoyugames/phaser-framework')。
940
+ * - 通过安全区辅助(this.safeArea / this.safeTop ...)规避刘海屏、小游戏胶囊按钮。
941
+ * - 通过设计分辨率辅助(this.designWidth/Height、this.centerX/Y、this.scaleToFit)
942
+ * 写出与具体屏幕无关、按设计稿坐标布局的场景。
943
+ *
944
+ * 设计分辨率取自 App.config(designWidth/designHeight),与 bootstrap 里 Phaser scale
945
+ * 配置一致(FIT + CENTER_BOTH),因此场景内可直接按设计稿坐标摆放元素。
946
+ */
947
+
948
+ declare abstract class BaseScene extends Phaser.Scene {
949
+ /** App 门面:业务在场景内访问网络/音频/存储等的统一入口 */
950
+ protected get app(): typeof App;
951
+ /**
952
+ * 场景级定时器(E4)。
953
+ * 业务在场景内用 this.scheduler.delay/interval/nextTick 启动定时,
954
+ * 场景 shutdown / destroy 时框架自动 clearAll(),避免"幽灵回调"。
955
+ */
956
+ protected readonly scheduler: Scheduler;
957
+ /**
958
+ * 需在场景关闭时清理的订阅句柄登记表(E4)。
959
+ * 收集 App.events.on / socket.onMessage / platform.lifecycle.onShow 等返回的 Unsubscribe,
960
+ * 场景 shutdown / destroy 时统一执行并清空,避免订阅泄漏。
961
+ */
962
+ private readonly _bindings;
963
+ /** 清理钩子是否已注册(防止同一实例重复注册) */
964
+ private _cleanupArmed;
965
+ /**
966
+ * E4 构造时挂上场景生命周期清理钩子。
967
+ *
968
+ * Phaser 场景实例可被复用(stop → start 重启):重启时不会再走 constructor,
969
+ * 但会再次触发 create。因此清理逻辑必须在每次 SHUTDOWN 都执行,并在执行后
970
+ * 重新登记(re-arm),保证下一轮生命周期同样被覆盖;DESTROY(实例被移除)只需一次。
971
+ */
972
+ constructor(config?: string | Phaser.Types.Scenes.SettingsConfig);
973
+ /**
974
+ * 登记需在场景关闭时清理的订阅(E4)。
975
+ * @param unsub App.events.on / socket.onMessage 等返回的 Unsubscribe
976
+ *
977
+ * 用法:this.bind(this.app.events.on('xxx', handler));
978
+ */
979
+ protected bind(unsub: Unsubscribe): void;
980
+ /** 注册 SHUTDOWN / DESTROY 清理钩子;SHUTDOWN 用 once + 自重注册以覆盖场景重启 */
981
+ private armCleanupHooks;
982
+ /** 执行全部清理:解绑所有 bind 的订阅 + 清空场景调度器,并重置登记表 */
983
+ private runCleanup;
984
+ /** 设计稿宽度(来自 App.config) */
985
+ protected get designWidth(): number;
986
+ /** 设计稿高度(来自 App.config) */
987
+ protected get designHeight(): number;
988
+ /** 设计稿水平中心 X */
989
+ protected get centerX(): number;
990
+ /** 设计稿垂直中心 Y */
991
+ protected get centerY(): number;
992
+ /** 当前平台安全区(刘海/胶囊规避用),来自 App.platform.info.safeArea */
993
+ protected get safeArea(): SafeArea;
994
+ protected get safeTop(): number;
995
+ protected get safeBottom(): number;
996
+ protected get safeLeft(): number;
997
+ protected get safeRight(): number;
998
+ /**
999
+ * 安全区内的可用矩形(以设计稿坐标系表示)。
1000
+ * 业务把关键 UI(返回按钮、顶栏)摆进这个矩形内即可避开遮挡。
1001
+ */
1002
+ protected get safeRect(): {
1003
+ x: number;
1004
+ y: number;
1005
+ width: number;
1006
+ height: number;
1007
+ };
1008
+ /**
1009
+ * 计算把内容(srcW × srcH)等比缩放以"填满"目标框(dstW × dstH)的缩放系数。
1010
+ * 用于背景图铺满、保持比例的图标缩放等。
1011
+ */
1012
+ protected scaleToFill(srcW: number, srcH: number, dstW: number, dstH: number): number;
1013
+ /**
1014
+ * 计算把内容等比缩放以"放进"目标框(不裁切)的缩放系数。
1015
+ */
1016
+ protected scaleToFit(srcW: number, srcH: number, dstW: number, dstH: number): number;
1017
+ }
1018
+
1019
+ /**
1020
+ * 基础按钮组件 Button(基于 Phaser.GameObjects.Container)。
1021
+ *
1022
+ * 真实可用,作为 UI 基础设施示范:
1023
+ * - 圆角背景(Graphics)+ 居中文本(Text)。
1024
+ * - 支持点击回调 onClick、按下态(缩放 + 变暗)、禁用态。
1025
+ * - 设置整体为可交互区域,处理 pointerover/out/down/up,跨平台触摸/鼠标一致。
1026
+ *
1027
+ * 业务用法:
1028
+ * const btn = new Button(this, { x, y, text: '开始', onClick: () => {...} });
1029
+ * this.add.existing(btn); // 或 Button 构造内已自动加入场景
1030
+ */
1031
+
1032
+ interface ButtonStyle {
1033
+ width?: number;
1034
+ height?: number;
1035
+ /** 背景颜色(常态) */
1036
+ backgroundColor?: number;
1037
+ /** 背景颜色(按下/悬停) */
1038
+ backgroundColorActive?: number;
1039
+ /** 圆角半径 */
1040
+ radius?: number;
1041
+ /** 文本颜色,如 '#ffffff' */
1042
+ textColor?: string;
1043
+ /** 字号 px */
1044
+ fontSize?: number;
1045
+ /** 字体族 */
1046
+ fontFamily?: string;
1047
+ }
1048
+ interface ButtonConfig extends ButtonStyle {
1049
+ x?: number;
1050
+ y?: number;
1051
+ text: string;
1052
+ /** 点击回调(按下并在按钮内抬起才触发) */
1053
+ onClick?: () => void;
1054
+ }
1055
+ declare class Button extends Phaser.GameObjects.Container {
1056
+ private readonly bg;
1057
+ private readonly label;
1058
+ private readonly style;
1059
+ private readonly onClick?;
1060
+ /** 是否禁用 */
1061
+ private _disabled;
1062
+ /** 指针是否在按下且仍停留在按钮内 */
1063
+ private pressed;
1064
+ constructor(scene: Phaser.Scene, config: ButtonConfig);
1065
+ /** 设置/更新按钮文案 */
1066
+ setText(text: string): this;
1067
+ /** 设置禁用态:禁用时变灰且不响应点击 */
1068
+ setDisabled(disabled: boolean): this;
1069
+ get disabled(): boolean;
1070
+ /** 绘制圆角背景(以中心为原点) */
1071
+ private drawBackground;
1072
+ /** 绑定指针交互:悬停/按下/抬起,实现按下态与点击触发 */
1073
+ private bindEvents;
1074
+ }
1075
+
1076
+ /**
1077
+ * 轻量状态管理(Store)。
1078
+ *
1079
+ * 目标:不引入任何外部库,提供一个浅合并 + 订阅 + 选择器的小型可观察状态容器。
1080
+ * 适合存放局部 UI 状态、玩法运行时数据等。复杂跨模块状态仍建议拆分多个 Store。
1081
+ *
1082
+ * 语义要点:
1083
+ * - set 用浅合并(Object.assign 语义),变更后同步通知所有订阅者。
1084
+ * - subscribe 监听整个 state 的任何变更。
1085
+ * - select 仅在所选子状态值变化(默认 Object.is 比较)时回调,避免无谓刷新。
1086
+ * - reset 回到 initial 的深拷贝(structuredClone,降级 JSON 兜底),
1087
+ * 保证多次 reset 互不污染,且不会把 initial 自身写脏。
1088
+ */
1089
+
1090
+ interface Store<T extends object> {
1091
+ /** 取当前状态(只读视图) */
1092
+ get(): Readonly<T>;
1093
+ /** 浅合并补丁;补丁可为对象或基于当前状态计算补丁的函数 */
1094
+ set(patch: Partial<T> | ((s: Readonly<T>) => Partial<T>)): void;
1095
+ /** 订阅整个状态变更;返回取消订阅句柄 */
1096
+ subscribe(fn: (s: Readonly<T>) => void): Unsubscribe;
1097
+ /**
1098
+ * 选择子状态,仅当其值变化时回调。
1099
+ * @param selector 从状态中选出关注的值
1100
+ * @param fn 值变化时回调,(新值, 旧值)
1101
+ * @param equals 自定义相等判断,默认 Object.is
1102
+ */
1103
+ select<K>(selector: (s: Readonly<T>) => K, fn: (v: K, prev: K) => void, equals?: (a: K, b: K) => boolean): Unsubscribe;
1104
+ /** 重置回 initial 的深拷贝 */
1105
+ reset(): void;
1106
+ }
1107
+ /**
1108
+ * 创建一个 Store。
1109
+ * @param initial 初始状态;内部持有其深拷贝,reset 时复用,不会改写传入对象。
1110
+ */
1111
+ declare function createStore<T extends object>(initial: T): Store<T>;
1112
+
1113
+ /**
1114
+ * 类型安全的事件总线 TypedEventBus<TMap>。
1115
+ *
1116
+ * 与全局 IEventBus(字符串事件名 + unknown payload)互补:当业务在一个模块内部
1117
+ * 定义一组"事件名 → payload 类型"的映射时,用本类可获得编译期类型检查——
1118
+ * emit 的 payload 类型必须与事件名匹配,on 的 handler 参数类型自动推断。
1119
+ *
1120
+ * 用法:
1121
+ * interface ShopEvents {
1122
+ * 'item:bought': { id: string; price: number };
1123
+ * 'panel:closed': void;
1124
+ * }
1125
+ * const bus = createTypedEventBus<ShopEvents>();
1126
+ * const off = bus.on('item:bought', (p) => console.log(p.id, p.price)); // p 自动推断
1127
+ * bus.emit('item:bought', { id: 'a', price: 10 }); // payload 受检
1128
+ * off(); // 取消订阅
1129
+ *
1130
+ * 实现独立(不依赖、不修改 EventBus),逻辑等价:
1131
+ * - 单事件 handler 数组保序;emit 时复制快照,回调内增删不影响本轮派发。
1132
+ * - once 用包装器:触发后先解绑自身再调原 handler,保证重入语义正确。
1133
+ */
1134
+
1135
+ /**
1136
+ * 类型化事件总线。TMap 形如 { 'a': PayloadA; 'b': PayloadB }。
1137
+ * payload 为 void 的事件,emit 时第二参可省略。
1138
+ */
1139
+ declare class TypedEventBus<TMap extends Record<string, unknown>> {
1140
+ /** 事件名 → 订阅列表 */
1141
+ private readonly map;
1142
+ /**
1143
+ * 订阅事件,返回取消订阅句柄。
1144
+ */
1145
+ on<K extends keyof TMap>(event: K, handler: (payload: TMap[K]) => void): Unsubscribe;
1146
+ /**
1147
+ * 订阅一次:触发后自动解绑。返回的句柄也可在触发前手动取消。
1148
+ */
1149
+ once<K extends keyof TMap>(event: K, handler: (payload: TMap[K]) => void): Unsubscribe;
1150
+ /**
1151
+ * 取消订阅:按原始 handler 匹配移除(只移除第一处匹配,与多次 on 的语义对应)。
1152
+ */
1153
+ off<K extends keyof TMap>(event: K, handler: (payload: TMap[K]) => void): void;
1154
+ /**
1155
+ * 触发事件。payload 类型由 TMap[K] 约束;void 事件可省略第二参。
1156
+ */
1157
+ emit<K extends keyof TMap>(event: K, payload: TMap[K]): void;
1158
+ /**
1159
+ * 清空订阅:传 event 只清该事件,不传清空全部。
1160
+ */
1161
+ clear(event?: keyof TMap): void;
1162
+ private add;
1163
+ private remove;
1164
+ }
1165
+ /**
1166
+ * 工厂函数:创建一个类型化事件总线实例。
1167
+ * const bus = createTypedEventBus<MyEvents>();
1168
+ */
1169
+ declare function createTypedEventBus<TMap extends Record<string, unknown>>(): TypedEventBus<TMap>;
1170
+
1171
+ /**
1172
+ * 通用对象池(ObjectPool)。
1173
+ *
1174
+ * 用途:高频创建/销毁的对象(子弹、特效、列表项节点等)复用,避免 GC 抖动。
1175
+ *
1176
+ * 语义:
1177
+ * - acquire:有空闲则取空闲对象;否则用 factory 新建。新建会计入总数(size)。
1178
+ * - release:可选 reset 后放回空闲队列;若空闲数已达 max,则丢弃(不放回),
1179
+ * 被丢弃对象交给 GC,size 相应减少(它不再由池持有)。
1180
+ * - preallocate:预创建 n 个空闲对象,摊平首次使用时的创建开销。
1181
+ * - clear:清空空闲队列并把已创建总数归零(池放弃对所有对象的持有)。
1182
+ *
1183
+ * 注意:池只跟踪"空闲"对象,acquire 出去的对象由调用方负责 release;
1184
+ * 调用方持有期间池不引用它们,符合"借出—归还"模型。
1185
+ */
1186
+ declare class ObjectPool<T> {
1187
+ /** 创建新对象的工厂 */
1188
+ private readonly factory;
1189
+ /** 归还时的重置回调(清状态),可选 */
1190
+ private readonly resetFn?;
1191
+ /** 空闲队列上限;超出则丢弃归还对象。<=0 视为不限制 */
1192
+ private readonly maxSize;
1193
+ /** 空闲对象栈(后进先出,缓存友好) */
1194
+ private readonly free;
1195
+ /** 已创建对象总数(含借出与空闲;被丢弃后相应减少) */
1196
+ private created;
1197
+ /**
1198
+ * @param factory 创建新对象的工厂函数
1199
+ * @param opts.reset 归还时重置对象状态的回调
1200
+ * @param opts.initial 初始预创建数量
1201
+ * @param opts.max 空闲队列上限(超出归还时丢弃);默认不限制
1202
+ */
1203
+ constructor(factory: () => T, opts?: {
1204
+ reset?: (o: T) => void;
1205
+ initial?: number;
1206
+ max?: number;
1207
+ });
1208
+ /** 已创建对象总数(借出 + 空闲) */
1209
+ get size(): number;
1210
+ /** 当前空闲(可被 acquire 直接复用)的对象数 */
1211
+ get available(): number;
1212
+ /** 借出一个对象:优先复用空闲对象,无则新建 */
1213
+ acquire(): T;
1214
+ /**
1215
+ * 归还一个对象。先 reset,再视空闲上限决定放回或丢弃。
1216
+ * 丢弃时该对象不再由池持有,created 相应减少。
1217
+ */
1218
+ release(o: T): void;
1219
+ /** 预创建 n 个空闲对象 */
1220
+ preallocate(n: number): void;
1221
+ /** 清空空闲队列并把已创建总数归零(池放弃所有持有) */
1222
+ clear(): void;
1223
+ }
1224
+
1225
+ /**
1226
+ * 统一错误模型(FrameworkError)。
1227
+ *
1228
+ * 全框架抛出的"可识别"错误统一用 FrameworkError,携带枚举错误码 + 原始 cause,
1229
+ * 便于上层按 code 分类处理(网络重试、超时提示、平台不支持降级等),
1230
+ * 也便于上报时聚合。第三方/原生异常包到 cause 里,不丢失栈信息。
1231
+ */
1232
+ /** 错误分类码 */
1233
+ declare enum ErrorCode {
1234
+ /** 未分类错误 */
1235
+ Unknown = 0,
1236
+ /** 网络错误(连接失败、DNS、断网等) */
1237
+ Network = 1,
1238
+ /** 超时 */
1239
+ Timeout = 2,
1240
+ /** HTTP 非 2xx 状态 */
1241
+ HttpStatus = 3,
1242
+ /** WebSocket/长连接被关闭 */
1243
+ SocketClosed = 4,
1244
+ /** 本地存储读写失败 */
1245
+ Storage = 5,
1246
+ /** 当前平台不支持该能力 */
1247
+ PlatformUnsupported = 6,
1248
+ /** 支付/内购失败 */
1249
+ Payment = 7,
1250
+ /** 广告失败 */
1251
+ Ads = 8,
1252
+ /** 配置(配置表/参数)错误 */
1253
+ Config = 9,
1254
+ /** 存档读写/解析错误 */
1255
+ Save = 10
1256
+ }
1257
+ declare class FrameworkError extends Error {
1258
+ /** 错误分类码 */
1259
+ readonly code: ErrorCode;
1260
+ /** 原始底层异常(第三方/原生错误),可选 */
1261
+ readonly cause?: unknown;
1262
+ constructor(code: ErrorCode, message: string, cause?: unknown);
1263
+ static network(message: string, cause?: unknown): FrameworkError;
1264
+ static timeout(message: string, cause?: unknown): FrameworkError;
1265
+ static httpStatus(message: string, cause?: unknown): FrameworkError;
1266
+ static socketClosed(message: string, cause?: unknown): FrameworkError;
1267
+ static storage(message: string, cause?: unknown): FrameworkError;
1268
+ static platformUnsupported(message: string, cause?: unknown): FrameworkError;
1269
+ static payment(message: string, cause?: unknown): FrameworkError;
1270
+ static ads(message: string, cause?: unknown): FrameworkError;
1271
+ static config(message: string, cause?: unknown): FrameworkError;
1272
+ static save(message: string, cause?: unknown): FrameworkError;
1273
+ static unknown(message: string, cause?: unknown): FrameworkError;
1274
+ }
1275
+ /** 类型守卫:判断一个值是否 FrameworkError */
1276
+ declare function isFrameworkError(e: unknown): e is FrameworkError;
1277
+
1278
+ /**
1279
+ * 网络消息层(MessageChannel)。
1280
+ *
1281
+ * 定位:在 ISocketClient(WebSocketClient / KcpClient 都实现它)之上,
1282
+ * 提供"类型路由 + 请求-响应 + 重连钩子"三件套,让业务以"发某类型消息、
1283
+ * 收某类型推送、发请求等响应"的语义收发,而不是手动 encode/decode + 自己配对 seq。
1284
+ *
1285
+ * 三大能力:
1286
+ * 1. 类型路由(on):服务端按 type 推送,业务按 type 注册处理器,解码后自动派发。
1287
+ * 2. 请求-响应(request):自增 seq 随消息发出,登记 pending Promise 与超时;
1288
+ * 收到带同一 seq 的响应即 resolve;超时则 reject(MessageTimeoutError)。
1289
+ * 3. 重连钩子(onReconnect):socket 第一次 open 记为"已连";之后每次 open
1290
+ * (重连成功)触发 onReconnect,供业务重新鉴权 / 重订阅 / 补偿状态。
1291
+ *
1292
+ * 编解码可插拔(MessageCodec),默认 JsonCodec(string JSON);
1293
+ * 二进制协议(如 protobuf over KCP)可自定义 codec 走 ArrayBuffer。
1294
+ *
1295
+ * 与底层 socket 的职责边界:重连本身由 ISocketClient(及其 Reconnector)负责,
1296
+ * MessageChannel 只在 socket 重新 open 时做应用层补偿(onReconnect),并管理
1297
+ * "重连后 pending 请求"的超时——重连不会自动重发已 pending 的请求(语义上
1298
+ * 旧请求是否还有效由业务决定),它们会照常按各自超时 reject,业务在 onReconnect 里重发。
1299
+ */
1300
+
1301
+ /** 消息信封:在 socket 原始字节之上的统一结构 */
1302
+ interface MessageEnvelope {
1303
+ /** 消息类型(路由键),如 'chat.msg' / 'room.join' */
1304
+ type: string;
1305
+ /**
1306
+ * 请求序号。request() 发出的消息带递增 seq,服务端响应须回带同一 seq;
1307
+ * send() 单向消息不带 seq(undefined)。
1308
+ */
1309
+ seq?: number;
1310
+ /** 业务负载(任意可被 codec 编码的数据) */
1311
+ payload: unknown;
1312
+ }
1313
+ /** 编解码器:把信封 ↔ socket 可发送的原始数据(string / ArrayBuffer)互转 */
1314
+ interface MessageCodec {
1315
+ encode(env: MessageEnvelope): string | ArrayBuffer;
1316
+ decode(raw: string | ArrayBuffer): MessageEnvelope;
1317
+ }
1318
+ /** MessageChannel 构造选项 */
1319
+ interface MessageChannelOptions {
1320
+ /** 自定义编解码器,默认 JsonCodec */
1321
+ codec?: MessageCodec;
1322
+ /** 请求超时(ms),默认 10000;<=0 表示不超时(谨慎使用,可能永久挂起) */
1323
+ requestTimeoutMs?: number;
1324
+ }
1325
+ /** 请求超时 / 通道关闭等导致 request 失败的错误(FrameworkError 风格:带 name) */
1326
+ declare class MessageError extends Error {
1327
+ constructor(message: string);
1328
+ }
1329
+ /**
1330
+ * 默认编解码器:信封 → JSON string。
1331
+ * - encode:JSON.stringify 整个信封。
1332
+ * - decode:仅支持 string 输入(JSON 文本);若底层是 ArrayBuffer(二进制协议),
1333
+ * 说明用错了 codec,decode 会抛错提示改用二进制 codec。
1334
+ */
1335
+ declare class JsonCodec implements MessageCodec {
1336
+ encode(env: MessageEnvelope): string;
1337
+ decode(raw: string | ArrayBuffer): MessageEnvelope;
1338
+ }
1339
+ declare class MessageChannel {
1340
+ private readonly socket;
1341
+ private readonly codec;
1342
+ private readonly requestTimeoutMs;
1343
+ /** 递增请求序号(从 1 开始,0 留作"无 seq"语义之外的保留) */
1344
+ private seqCounter;
1345
+ /** 等待响应的请求:seq → pending */
1346
+ private readonly pending;
1347
+ /** 按 type 路由的服务端推送监听器:type → 处理器集合 */
1348
+ private readonly typeListeners;
1349
+ /** 重连(非首次 open)监听器 */
1350
+ private readonly reconnectListeners;
1351
+ /** 是否已发生过首次 open(用于区分"首连"与"重连") */
1352
+ private opened;
1353
+ /** 已注册到 socket 的事件取消句柄,close 时统一清理 */
1354
+ private readonly socketUnsubs;
1355
+ /** 是否已 close(关闭后拒绝新请求 / 不再派发) */
1356
+ private closed;
1357
+ constructor(socket: ISocketClient, opts?: MessageChannelOptions);
1358
+ /** 建立连接(委托底层 socket.connect) */
1359
+ connect(): Promise<void>;
1360
+ /**
1361
+ * 发送请求并等待同 seq 响应。
1362
+ * @template TRes 期望响应 payload 类型
1363
+ * @param type 消息类型
1364
+ * @param payload 请求负载
1365
+ * @returns 服务端响应的 payload(按 TRes 断言)
1366
+ * @throws MessageError 超时 / 通道已关闭 / 发送失败
1367
+ */
1368
+ request<TRes = unknown>(type: string, payload?: unknown): Promise<TRes>;
1369
+ /** 发送单向消息(不带 seq,不等响应) */
1370
+ send(type: string, payload?: unknown): void;
1371
+ /**
1372
+ * 订阅某类型的服务端推送(无 seq 的消息,或带 seq 但无匹配 pending 的消息)。
1373
+ * @template T 推送 payload 类型
1374
+ * @returns 取消订阅句柄
1375
+ */
1376
+ on<T = unknown>(type: string, handler: (payload: T) => void): Unsubscribe;
1377
+ /**
1378
+ * 订阅"重连成功"事件:socket 首次 open 后,再次 open 时触发。
1379
+ * 业务在此重新鉴权 / 重新订阅 / 补偿状态。
1380
+ * @returns 取消订阅句柄
1381
+ */
1382
+ onReconnect(fn: () => void): Unsubscribe;
1383
+ /**
1384
+ * 关闭通道:关闭底层 socket、解绑事件、拒绝所有未决请求、清空监听器。
1385
+ * 关闭后本通道不可再用(再次 connect/request/send 会抛错)。
1386
+ */
1387
+ close(): void;
1388
+ /** 接线底层 socket 的 open / message 事件 */
1389
+ private bindSocket;
1390
+ /** socket open:首次记为"已连",之后每次都视为"重连"并触发 onReconnect */
1391
+ private handleOpen;
1392
+ /** 收到底层数据:解码 → 有 seq 命中 pending 则 resolve,否则按 type 派发 */
1393
+ private handleMessage;
1394
+ /** 取下一个请求序号 */
1395
+ private nextSeq;
1396
+ /** 清理某个 pending(清超时定时器并从表中移除) */
1397
+ private clearPending;
1398
+ }
1399
+
1400
+ /**
1401
+ * 平台上下文:全局持有"当前平台实现"的唯一入口。
1402
+ *
1403
+ * - 由平台入口(main.<platform>.ts)在启动时通过 bootstrap 注入。
1404
+ * - 网络层 / 服务层通过 PlatformContext.current 拿到平台能力,
1405
+ * 从而与具体平台解耦。
1406
+ */
1407
+
1408
+ declare const PlatformContext: {
1409
+ /** 注入当前平台实现(仅 bootstrap 调用一次) */
1410
+ set(platform: IPlatform): void;
1411
+ /** 当前平台实现 */
1412
+ readonly current: IPlatform;
1413
+ /** 是否已初始化 */
1414
+ readonly ready: boolean;
1415
+ /** 仅供测试重置 */
1416
+ _resetForTest(): void;
1417
+ };
1418
+
1419
+ /**
1420
+ * 网络管理器(INetManager)。
1421
+ *
1422
+ * 作为网络层的工厂门面,业务通过 App.net 拿到它:
1423
+ * App.net.http() 创建一个 HTTP 客户端
1424
+ * App.net.websocket() 创建一个 WebSocket 长连接客户端
1425
+ * App.net.kcp() 创建一个 KCP 长连接客户端
1426
+ * App.net.defaultHttp 框架级默认 HTTP 单例(baseURL = 构造时传入的 apiBaseURL)
1427
+ *
1428
+ * 构造签名与服务层 bootstrap 约定一致:
1429
+ * new NetManager({ apiBaseURL })
1430
+ */
1431
+
1432
+ /** NetManager 构造参数 */
1433
+ interface NetManagerOptions {
1434
+ /** 默认 HTTP 客户端使用的业务后端基址 */
1435
+ apiBaseURL?: string;
1436
+ }
1437
+ declare class NetManager implements INetManager {
1438
+ private readonly apiBaseURL;
1439
+ /** defaultHttp 懒加载单例 */
1440
+ private _defaultHttp;
1441
+ constructor(opts?: NetManagerOptions);
1442
+ /** 创建一个独立的 HTTP 客户端;不传 config 时默认带上全局 apiBaseURL */
1443
+ http(config?: HttpClientConfig): IHttpClient;
1444
+ /** 创建 WebSocket 长连接客户端 */
1445
+ websocket(config: SocketClientConfig): ISocketClient;
1446
+ /** 创建 KCP 长连接客户端 */
1447
+ kcp(config: KcpClientConfig): ISocketClient;
1448
+ /** 框架级默认 HTTP 单例(读取构造时的 apiBaseURL) */
1449
+ get defaultHttp(): IHttpClient;
1450
+ }
1451
+
1452
+ /**
1453
+ * HTTP 客户端实现(IHttpClient)。
1454
+ *
1455
+ * 设计要点:
1456
+ * - 不直接用 fetch / wx.request,而是通过 PlatformContext.current.net.request 发请求,
1457
+ * 从而在 web / TikTok / 微信 / Facebook / Capacitor 上行为一致。
1458
+ * - 支持 baseURL 拼接、默认 headers、超时、幂等重试(指数退避)、
1459
+ * 请求 / 响应 / 错误三类拦截器(可链式 use)。
1460
+ * - request<T> 按 responseType 解析并返回 data;get/post/put/delete 为便捷封装。
1461
+ */
1462
+
1463
+ declare class HttpClient implements IHttpClient {
1464
+ private readonly baseURL;
1465
+ private readonly headers;
1466
+ private readonly timeout;
1467
+ private readonly retries;
1468
+ /** 拦截器按 use 顺序累积;请求拦截顺序执行,响应拦截顺序执行 */
1469
+ private readonly requestInterceptors;
1470
+ private readonly responseInterceptors;
1471
+ private readonly errorInterceptors;
1472
+ constructor(config?: HttpClientConfig);
1473
+ /** 设置 / 覆盖一个默认请求头(作用于后续所有请求) */
1474
+ setHeader(key: string, value: string): void;
1475
+ /** 注册拦截器,可链式多次调用累积 */
1476
+ use(interceptors: HttpInterceptors): void;
1477
+ /** 核心请求方法:拼 URL、合并 header、跑拦截器、超时、重试、解析返回 */
1478
+ request<T = unknown>(options: HttpRequestOptions): Promise<T>;
1479
+ get<T = unknown>(url: string, options?: Partial<HttpRequestOptions>): Promise<T>;
1480
+ post<T = unknown>(url: string, body?: unknown, options?: Partial<HttpRequestOptions>): Promise<T>;
1481
+ put<T = unknown>(url: string, body?: unknown, options?: Partial<HttpRequestOptions>): Promise<T>;
1482
+ delete<T = unknown>(url: string, options?: Partial<HttpRequestOptions>): Promise<T>;
1483
+ /** 发一次底层请求(经平台层),不含重试 / 拦截器 */
1484
+ private sendOnce;
1485
+ /** baseURL + path 拼接:绝对 URL 原样返回,相对 path 拼基址(处理斜杠) */
1486
+ private resolveUrl;
1487
+ /** 第 attempt 次失败后的退避时长:base 100ms 指数增长,封顶 5s */
1488
+ private backoffMs;
1489
+ }
1490
+ /** 携带 HTTP 状态码与响应体的错误类型 */
1491
+ declare class HttpError extends Error {
1492
+ readonly status: number;
1493
+ readonly data: unknown;
1494
+ constructor(message: string, status: number, data: unknown);
1495
+ }
1496
+
1497
+ /**
1498
+ * WebSocket 长连接客户端(ISocketClient)。
1499
+ *
1500
+ * 设计要点:
1501
+ * - 不直接 new WebSocket,而是通过 PlatformContext.current.net.createSocket 拿到
1502
+ * IPlatformSocket(底层是 WebSocket / wx.connectSocket),从而跨平台一致。
1503
+ * - connect() 返回 Promise,在 open 时 resolve、连接失败时 reject。
1504
+ * - 自动重连:autoReconnect / maxReconnect / reconnectIntervalMs 指数退避(复用 Reconnector)。
1505
+ * - 心跳:heartbeatIntervalMs > 0 时定时发送 heartbeatPayload。
1506
+ * - state 状态机:connecting / open / closing / closed;主动 close() 不触发重连。
1507
+ */
1508
+
1509
+ declare class WebSocketClient implements ISocketClient {
1510
+ private readonly url;
1511
+ private readonly heartbeatIntervalMs;
1512
+ private readonly heartbeatPayload;
1513
+ private socket;
1514
+ private _state;
1515
+ /** 当前底层 socket 上注册的取消订阅句柄,断开时统一清理 */
1516
+ private socketUnsubs;
1517
+ /** 心跳定时器 */
1518
+ private heartbeatTimer;
1519
+ /** 是否为主动关闭(主动关闭不重连) */
1520
+ private manualClose;
1521
+ /** 当前 connect() 的 resolve/reject(用于把首次连接结果回传给调用方) */
1522
+ private pendingConnect;
1523
+ private readonly reconnector;
1524
+ private readonly openListeners;
1525
+ private readonly messageListeners;
1526
+ private readonly errorListeners;
1527
+ private readonly closeListeners;
1528
+ constructor(config: SocketClientConfig);
1529
+ get state(): SocketState;
1530
+ /** 建立连接:open 时 resolve,失败时 reject。重连由内部自动处理。 */
1531
+ connect(): Promise<void>;
1532
+ send(data: string | ArrayBuffer): void;
1533
+ /** 主动关闭:停心跳、取消重连、关闭底层 socket */
1534
+ close(): void;
1535
+ onOpen(cb: () => void): Unsubscribe;
1536
+ onMessage(cb: (data: string | ArrayBuffer) => void): Unsubscribe;
1537
+ onError(cb: (err: Error) => void): Unsubscribe;
1538
+ onClose(cb: () => void): Unsubscribe;
1539
+ /** 打开一条底层 socket 并接线事件;返回的 Promise 在 open 时 resolve */
1540
+ private openSocket;
1541
+ /** 底层 close 事件处理:停心跳、回调,非主动关闭则安排重连 */
1542
+ private handleClose;
1543
+ /** 标记彻底关闭并通知业务 */
1544
+ private setClosed;
1545
+ /** 解绑当前 socket 的所有事件订阅 */
1546
+ private teardownSocket;
1547
+ private startHeartbeat;
1548
+ private stopHeartbeat;
1549
+ private emitOpen;
1550
+ private emitMessage;
1551
+ private emitError;
1552
+ private emitClose;
1553
+ }
1554
+
1555
+ /**
1556
+ * KCP 长连接客户端(ISocketClient,KCP 语义)。
1557
+ *
1558
+ * 设计要点:
1559
+ * - KCP 本身只是一个 ARQ 算法层(见 ./kcp/ikcp.ts),需要一条底层 unreliable 数据报通道。
1560
+ * 浏览器 / 小游戏没有原生 UDP,这里默认用 PlatformContext.current.net.createSocket
1561
+ * 建立一条 binaryType=arraybuffer 的 WebSocket 当作"数据报"通道承载 KCP 包。
1562
+ * - 接线:
1563
+ * kcp.output 回调 → transport.send(ArrayBuffer) (KCP 产生的包发到网络)
1564
+ * transport.onMessage(ab) → kcp.input(bytes) (网络收到的包喂给 KCP)
1565
+ * setInterval(intervalMs) → kcp.update(now) + 循环 kcp.recv() 把就绪消息吐给业务
1566
+ * - send(string|ArrayBuffer):string 用 TextEncoder 转 bytes 再 kcp.send。
1567
+ * - 应用 KcpClientConfig:conv / nodelay / intervalMs / resend / nc / sndWnd / rcvWnd
1568
+ * 映射到 kcp.nodelay() 与 kcp.wndsize()。
1569
+ * - 自动重连复用 Reconnector(与 WebSocketClient 同一套指数退避策略)。
1570
+ *
1571
+ * —— Capacitor 原生 UDP 替换点 ——
1572
+ * 默认 transport 由 createDefaultTransport() 基于 WebSocket 构造。若在 Capacitor 上接入
1573
+ * 原生 UDP 插件(真正的 unreliable datagram),只需提供一个实现 IKcpTransport 的对象,
1574
+ * 通过构造参数 transportFactory 注入即可,KcpClient 其余逻辑零改动:
1575
+ *
1576
+ * new KcpClient(config, () => new CapacitorUdpTransport(config.url));
1577
+ *
1578
+ * IKcpTransport 只要求 send(ArrayBuffer) + onMessage/onOpen/onError/onClose + close,
1579
+ * 与具体是 WebSocket 还是原生 UDP 无关。
1580
+ */
1581
+
1582
+ /**
1583
+ * KCP 底层数据报传输抽象。默认实现包裹 IPlatformSocket(WebSocket),
1584
+ * Capacitor 原生 UDP 实现同样满足此接口即可注入替换。
1585
+ */
1586
+ interface IKcpTransport {
1587
+ /** 把一个 KCP 包发到网络(原始字节,不可靠语义由 KCP 负责) */
1588
+ send(data: ArrayBuffer): void;
1589
+ /** 关闭底层通道 */
1590
+ close(): void;
1591
+ onOpen(cb: () => void): Unsubscribe;
1592
+ onMessage(cb: (data: ArrayBuffer) => void): Unsubscribe;
1593
+ onError(cb: (err: Error) => void): Unsubscribe;
1594
+ onClose(cb: () => void): Unsubscribe;
1595
+ }
1596
+ /** 传输工厂:给定 url 返回一条传输通道。默认基于 WebSocket。 */
1597
+ type KcpTransportFactory = (url: string) => IKcpTransport;
1598
+ declare class KcpClient implements ISocketClient {
1599
+ private readonly url;
1600
+ private readonly conv;
1601
+ private readonly intervalMs;
1602
+ private readonly cfgNodelay;
1603
+ private readonly cfgResend;
1604
+ private readonly cfgNc;
1605
+ private readonly cfgSndWnd?;
1606
+ private readonly cfgRcvWnd?;
1607
+ private readonly transportFactory;
1608
+ private kcp;
1609
+ private transport;
1610
+ private updateTimer;
1611
+ private _state;
1612
+ private manualClose;
1613
+ private transportUnsubs;
1614
+ private pendingConnect;
1615
+ private readonly reconnector;
1616
+ private readonly textEncoder;
1617
+ private readonly openListeners;
1618
+ private readonly messageListeners;
1619
+ private readonly errorListeners;
1620
+ private readonly closeListeners;
1621
+ constructor(config: KcpClientConfig, transportFactory?: KcpTransportFactory);
1622
+ get state(): SocketState;
1623
+ connect(): Promise<void>;
1624
+ send(data: string | ArrayBuffer): void;
1625
+ close(): void;
1626
+ onOpen(cb: () => void): Unsubscribe;
1627
+ onMessage(cb: (data: string | ArrayBuffer) => void): Unsubscribe;
1628
+ onError(cb: (err: Error) => void): Unsubscribe;
1629
+ onClose(cb: () => void): Unsubscribe;
1630
+ /** 打开底层传输并初始化 KCP;Promise 在传输 open 时 resolve */
1631
+ private openTransport;
1632
+ /** 初始化 KCP 控制块并接线 output、应用配置 */
1633
+ private setupKcp;
1634
+ /** 周期定时器:驱动 kcp.update 并把就绪消息吐给业务 */
1635
+ private startUpdateLoop;
1636
+ private stopUpdateLoop;
1637
+ /** 驱动 KCP 一拍:update(now) + 循环 recv() 把所有就绪消息发给业务 */
1638
+ private pump;
1639
+ private handleClose;
1640
+ private setClosed;
1641
+ private teardownTransport;
1642
+ /** 当前毫秒时间戳 */
1643
+ private now;
1644
+ /**
1645
+ * 默认传输实现:基于 PlatformContext 的 IPlatformSocket(WebSocket / wx.connectSocket)。
1646
+ * binaryType=arraybuffer,把它当作 KCP 的"数据报"通道。
1647
+ *
1648
+ * Capacitor 原生 UDP 替换:实现一个同样满足 IKcpTransport 的类(send 走原生 UDP socket,
1649
+ * onMessage 把收到的 datagram 转 ArrayBuffer 回调),通过构造参数 transportFactory 注入。
1650
+ */
1651
+ private createDefaultTransport;
1652
+ private emitOpen;
1653
+ private emitMessage;
1654
+ private emitError;
1655
+ private emitClose;
1656
+ }
1657
+
1658
+ /**
1659
+ * 多平台打包配置 PlatformConfig。
1660
+ *
1661
+ * 这是"脚手架 / CLI 构建期"读取的平台元信息单一来源:用户项目在根目录的
1662
+ * platform.config.ts 里 `export default { ... } satisfies PlatformConfig`,
1663
+ * pf CLI 据此为各平台生成外壳(index.html / fbapp-config.json /
1664
+ * game.json / project.config.json / capacitor.config.ts 等)与注入构建变量。
1665
+ *
1666
+ * 设计原则:
1667
+ * - 每个平台一个可选子配置(只构建用到的平台才需填)。
1668
+ * - 字段对应各平台后台/外壳里"必须由开发者填写"的占位项,命名贴合官方术语。
1669
+ * - common 收敛跨平台共享项(后端基址、广告位),避免在每个平台里重复写。
1670
+ *
1671
+ * 注意:本类型描述的是**打包/平台外壳配置**,与运行时业务配置 AppConfig 互不相同
1672
+ * (AppConfig 注入 startGame,PlatformConfig 喂给 CLI/构建)。
1673
+ */
1674
+ /** 屏幕方向 */
1675
+ type Orientation = 'portrait' | 'landscape';
1676
+ /** 跨平台共享的广告位默认配置(可被各平台覆盖) */
1677
+ interface AdsConfig {
1678
+ /** 激励视频默认广告位 id */
1679
+ rewardedUnitId?: string;
1680
+ /** 插屏默认广告位 id */
1681
+ interstitialUnitId?: string;
1682
+ /** banner 默认广告位 id */
1683
+ bannerUnitId?: string;
1684
+ }
1685
+ /** Web(标准 H5)平台配置 */
1686
+ interface WebPlatformConfig {
1687
+ /** 页面标题(注入 index.html <title>) */
1688
+ title?: string;
1689
+ /** 屏幕方向(用于 viewport / meta 提示) */
1690
+ orientation?: Orientation;
1691
+ /** 部署 base 路径(子目录部署时设置,如 '/game/') */
1692
+ base?: string;
1693
+ /** 该平台业务后端基址(覆盖 common.apiBaseURL) */
1694
+ apiBaseURL?: string;
1695
+ /** 该平台广告位配置(覆盖 common.ads) */
1696
+ ads?: AdsConfig;
1697
+ }
1698
+ /** TikTok Mini Games 平台配置 */
1699
+ interface TikTokPlatformConfig {
1700
+ /** TikTok 小游戏 SDK 脚本地址(注入 index.html) */
1701
+ sdkUrl?: string;
1702
+ /** 页面标题 */
1703
+ title?: string;
1704
+ /** 屏幕方向 */
1705
+ orientation?: Orientation;
1706
+ /** 业务后端基址(覆盖 common.apiBaseURL) */
1707
+ apiBaseURL?: string;
1708
+ /** 广告位配置(覆盖 common.ads) */
1709
+ ads?: AdsConfig;
1710
+ }
1711
+ /** 微信小游戏平台配置 */
1712
+ interface WeChatPlatformConfig {
1713
+ /** 微信小游戏 AppID(微信公众平台申请,形如 wxXXXXXXXX) */
1714
+ appid: string;
1715
+ /** 项目名称(写入 project.config.json projectname) */
1716
+ projectname?: string;
1717
+ /** 屏幕方向(写入 game.json deviceOrientation) */
1718
+ orientation?: Orientation;
1719
+ /** 业务后端基址(覆盖 common.apiBaseURL;须在微信后台配 request 合法域名) */
1720
+ apiBaseURL?: string;
1721
+ /** 广告位配置(覆盖 common.ads;微信广告位 id 形如 adunit-xxx) */
1722
+ ads?: AdsConfig;
1723
+ }
1724
+ /** Facebook Instant Games 平台配置 */
1725
+ interface FacebookPlatformConfig {
1726
+ /** Facebook 应用 ID(developers.facebook.com 创建 Instant Game 后获得) */
1727
+ appId: string;
1728
+ /** 应用名称 */
1729
+ appName?: string;
1730
+ /** Instant Games SDK 版本(写入 fbapp-config / index.html,如 '7.1') */
1731
+ fbSdkVersion?: string;
1732
+ /** 屏幕方向(写入 fbapp-config orientation,大写) */
1733
+ orientation?: Orientation;
1734
+ /** 业务后端基址(覆盖 common.apiBaseURL) */
1735
+ apiBaseURL?: string;
1736
+ /** 广告位配置(覆盖 common.ads) */
1737
+ ads?: AdsConfig;
1738
+ }
1739
+ /** Capacitor(iOS + Android 原生 App)平台配置 */
1740
+ interface CapacitorPlatformConfig {
1741
+ /** 应用包名/反向域名(写入 capacitor.config appId,如 com.example.app) */
1742
+ appId: string;
1743
+ /** 应用显示名(写入 capacitor.config appName) */
1744
+ appName: string;
1745
+ /** 屏幕方向 */
1746
+ orientation?: Orientation;
1747
+ /** 业务后端基址(覆盖 common.apiBaseURL) */
1748
+ apiBaseURL?: string;
1749
+ /** 广告位配置(覆盖 common.ads) */
1750
+ ads?: AdsConfig;
1751
+ }
1752
+ /**
1753
+ * 多平台打包配置根类型。
1754
+ *
1755
+ * 用户项目以 `satisfies PlatformConfig` 约束 platform.config.ts 默认导出。
1756
+ * 各平台子配置均可选——只为实际构建的平台填写即可。
1757
+ */
1758
+ interface PlatformConfig {
1759
+ /** 跨平台共享项(各平台同名字段覆盖之) */
1760
+ common?: {
1761
+ /** 默认业务后端基址 */
1762
+ apiBaseURL?: string;
1763
+ /** 默认广告位配置 */
1764
+ ads?: AdsConfig;
1765
+ };
1766
+ /** Web(标准 H5) */
1767
+ web?: WebPlatformConfig;
1768
+ /** TikTok Mini Games */
1769
+ tiktok?: TikTokPlatformConfig;
1770
+ /** 微信小游戏 */
1771
+ wechat?: WeChatPlatformConfig;
1772
+ /** Facebook Instant Games */
1773
+ facebook?: FacebookPlatformConfig;
1774
+ /** Capacitor 原生 App */
1775
+ capacitor?: CapacitorPlatformConfig;
1776
+ }
1777
+
1778
+ export { AdResult, type AdsConfig, App, type AppConfig, BaseModule, BasePanel, BaseScene, Button, type ButtonConfig, type ButtonStyle, type CapacitorPlatformConfig, ConfigTable, ConfigTableManager, ErrorCode, type FacebookPlatformConfig, FrameworkError, Handler, HttpClient, type HttpClientConfig, HttpError, type HttpInterceptors, HttpRequestOptions, I18n, type I18nOptions, type IAccountService, type IAdsManager, type IAnalyticsManager, type IApp, type IAssetLoader, type IAudioManager, type IConfigService, type IEventBus, type IHttpClient, type IKcpTransport, type ILogger, type INetManager, type IPanelManager, type IPaymentManager, IPlatform, IPlatformLifecycle, type ISceneManager, type ISocketClient, type IStorageManager, JsonCodec, KcpClient, type KcpClientConfig, type KcpTransportFactory, type LayerName, Layers, LogLevel, LoginResult, MessageChannel, type MessageChannelOptions, type MessageCodec, type MessageEnvelope, MessageError, ModuleRegistry, NetManager, type NetManagerOptions, type NetProtocol, ObjectPool, type Orientation, type PanelCtor, PanelManager, type PlatformConfig, PlatformContext, PurchaseResult, SafeArea, SaveManager, type SaveSlot, type SaveSlotOptions, type ScheduleHandle, Scheduler, type SocketClientConfig, type SocketState, type StartGameOptions, type Store, type TikTokPlatformConfig, TypedEventBus, Unsubscribe, type WeChatPlatformConfig, type WebPlatformConfig, WebSocketClient, createStore, createTypedEventBus, isFrameworkError, normalizeLocale, startGame };