@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.
package/dist/index.js ADDED
@@ -0,0 +1,3790 @@
1
+ import { PlatformUnsupportedError } from './chunk-II3JM4R3.js';
2
+ export { PlatformUnsupportedError } from './chunk-II3JM4R3.js';
3
+ import { __publicField } from './chunk-PKBMQBKP.js';
4
+ import Phaser4 from 'phaser';
5
+
6
+ // src/App.ts
7
+ var parts = null;
8
+ function require2(key) {
9
+ if (!parts) {
10
+ throw new Error(
11
+ `[App] \u8BBF\u95EE App.${String(key)} \u5931\u8D25:App \u5C1A\u672A\u521D\u59CB\u5316\u3002\u8BF7\u786E\u8BA4\u5DF2\u901A\u8FC7 startGame()(@maoyugames/phaser-framework \u7684 startGame)\u542F\u52A8,\u4E1A\u52A1\u4EE3\u7801\u4E0D\u8981\u5728 bootstrap \u5B8C\u6210\u524D\u8BBF\u95EE App\u3002`
12
+ );
13
+ }
14
+ return parts[key];
15
+ }
16
+ var App = {
17
+ get platform() {
18
+ return require2("platform");
19
+ },
20
+ get net() {
21
+ return require2("net");
22
+ },
23
+ get events() {
24
+ return require2("events");
25
+ },
26
+ get log() {
27
+ return require2("log");
28
+ },
29
+ get config() {
30
+ return require2("config");
31
+ },
32
+ get audio() {
33
+ return require2("audio");
34
+ },
35
+ get storage() {
36
+ return require2("storage");
37
+ },
38
+ get assets() {
39
+ return require2("assets");
40
+ },
41
+ get ads() {
42
+ return require2("ads");
43
+ },
44
+ get payment() {
45
+ return require2("payment");
46
+ },
47
+ get analytics() {
48
+ return require2("analytics");
49
+ },
50
+ get account() {
51
+ return require2("account");
52
+ },
53
+ get scenes() {
54
+ return require2("scenes");
55
+ },
56
+ get i18n() {
57
+ return require2("i18n");
58
+ },
59
+ get save() {
60
+ return require2("save");
61
+ },
62
+ get tables() {
63
+ return require2("tables");
64
+ },
65
+ get panels() {
66
+ return require2("panels");
67
+ },
68
+ get modules() {
69
+ return require2("modules");
70
+ },
71
+ get lifecycle() {
72
+ return require2("lifecycle");
73
+ }
74
+ };
75
+ function __initApp(injected) {
76
+ if (parts) {
77
+ console.warn("[App] \u5DF2\u521D\u59CB\u5316,\u91CD\u590D __initApp \u88AB\u5FFD\u7565");
78
+ return;
79
+ }
80
+ parts = injected;
81
+ }
82
+
83
+ // src/platform/PlatformContext.ts
84
+ var _current = null;
85
+ var PlatformContext = {
86
+ /** 注入当前平台实现(仅 bootstrap 调用一次) */
87
+ set(platform) {
88
+ if (_current) {
89
+ console.warn("[PlatformContext] \u5E73\u53F0\u5DF2\u521D\u59CB\u5316,\u91CD\u590D\u6CE8\u5165\u5C06\u88AB\u5FFD\u7565");
90
+ return;
91
+ }
92
+ _current = platform;
93
+ },
94
+ /** 当前平台实现 */
95
+ get current() {
96
+ if (!_current) {
97
+ throw new Error(
98
+ "[PlatformContext] \u5E73\u53F0\u5C1A\u672A\u521D\u59CB\u5316\u3002\u8BF7\u786E\u8BA4\u901A\u8FC7 startGame() \u542F\u52A8,\u4E14\u5E73\u53F0\u5165\u53E3\u5DF2\u6CE8\u5165\u5E73\u53F0\u5B9E\u73B0\u3002"
99
+ );
100
+ }
101
+ return _current;
102
+ },
103
+ /** 是否已初始化 */
104
+ get ready() {
105
+ return _current !== null;
106
+ },
107
+ /** 仅供测试重置 */
108
+ _resetForTest() {
109
+ _current = null;
110
+ }
111
+ };
112
+
113
+ // src/net/HttpClient.ts
114
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
115
+ "GET",
116
+ "HEAD",
117
+ "PUT",
118
+ "DELETE"
119
+ ]);
120
+ var HttpClient = class {
121
+ constructor(config = {}) {
122
+ __publicField(this, "baseURL");
123
+ __publicField(this, "headers");
124
+ __publicField(this, "timeout");
125
+ __publicField(this, "retries");
126
+ /** 拦截器按 use 顺序累积;请求拦截顺序执行,响应拦截顺序执行 */
127
+ __publicField(this, "requestInterceptors", []);
128
+ __publicField(this, "responseInterceptors", []);
129
+ __publicField(this, "errorInterceptors", []);
130
+ this.baseURL = config.baseURL ?? "";
131
+ this.headers = { ...config.headers ?? {} };
132
+ this.timeout = config.timeout ?? 0;
133
+ this.retries = config.retries ?? 0;
134
+ }
135
+ /** 设置 / 覆盖一个默认请求头(作用于后续所有请求) */
136
+ setHeader(key, value) {
137
+ this.headers[key] = value;
138
+ }
139
+ /** 注册拦截器,可链式多次调用累积 */
140
+ use(interceptors) {
141
+ if (interceptors.request) this.requestInterceptors.push(interceptors.request);
142
+ if (interceptors.response) this.responseInterceptors.push(interceptors.response);
143
+ if (interceptors.error) this.errorInterceptors.push(interceptors.error);
144
+ }
145
+ /** 核心请求方法:拼 URL、合并 header、跑拦截器、超时、重试、解析返回 */
146
+ async request(options) {
147
+ let opts = {
148
+ ...options,
149
+ url: this.resolveUrl(options.url),
150
+ method: options.method ?? "GET",
151
+ headers: { ...this.headers, ...options.headers ?? {} },
152
+ responseType: options.responseType ?? "json",
153
+ timeout: options.timeout ?? this.timeout
154
+ };
155
+ for (const interceptor of this.requestInterceptors) {
156
+ opts = await interceptor(opts);
157
+ }
158
+ const method = opts.method ?? "GET";
159
+ const canRetry = IDEMPOTENT_METHODS.has(method);
160
+ const maxAttempts = canRetry ? this.retries + 1 : 1;
161
+ let lastError;
162
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
163
+ try {
164
+ const response = await this.sendOnce(opts);
165
+ if (!response.ok) {
166
+ throw new HttpError(
167
+ `HTTP ${response.status}`,
168
+ response.status,
169
+ response.data
170
+ );
171
+ }
172
+ let data = response.data;
173
+ for (const interceptor of this.responseInterceptors) {
174
+ data = await interceptor(data, response.status);
175
+ }
176
+ return data;
177
+ } catch (e) {
178
+ lastError = e instanceof Error ? e : new Error(String(e));
179
+ if (attempt < maxAttempts - 1 && isRetryableError(lastError)) {
180
+ await delay(this.backoffMs(attempt));
181
+ continue;
182
+ }
183
+ break;
184
+ }
185
+ }
186
+ const finalError = lastError ?? new Error("HTTP \u8BF7\u6C42\u5931\u8D25(\u672A\u77E5\u539F\u56E0)");
187
+ for (const interceptor of this.errorInterceptors) {
188
+ try {
189
+ interceptor(finalError);
190
+ } catch {
191
+ }
192
+ }
193
+ throw finalError;
194
+ }
195
+ get(url, options = {}) {
196
+ return this.request({ ...options, url, method: "GET" });
197
+ }
198
+ post(url, body, options = {}) {
199
+ return this.request({ ...options, url, method: "POST", body });
200
+ }
201
+ put(url, body, options = {}) {
202
+ return this.request({ ...options, url, method: "PUT", body });
203
+ }
204
+ delete(url, options = {}) {
205
+ return this.request({ ...options, url, method: "DELETE" });
206
+ }
207
+ /** 发一次底层请求(经平台层),不含重试 / 拦截器 */
208
+ async sendOnce(opts) {
209
+ return PlatformContext.current.net.request(opts);
210
+ }
211
+ /** baseURL + path 拼接:绝对 URL 原样返回,相对 path 拼基址(处理斜杠) */
212
+ resolveUrl(url) {
213
+ if (/^https?:\/\//i.test(url)) return url;
214
+ if (!this.baseURL) return url;
215
+ const base = this.baseURL.replace(/\/+$/, "");
216
+ const path = url.replace(/^\/+/, "");
217
+ return path ? `${base}/${path}` : base;
218
+ }
219
+ /** 第 attempt 次失败后的退避时长:base 100ms 指数增长,封顶 5s */
220
+ backoffMs(attempt) {
221
+ return Math.min(100 * Math.pow(2, attempt), 5e3);
222
+ }
223
+ };
224
+ var HttpError = class extends Error {
225
+ constructor(message, status, data) {
226
+ super(message);
227
+ __publicField(this, "status", status);
228
+ __publicField(this, "data", data);
229
+ this.name = "HttpError";
230
+ }
231
+ };
232
+ function isRetryableError(error) {
233
+ if (error.name === "AbortError") return false;
234
+ if (error instanceof HttpError) {
235
+ return error.status >= 500 && error.status < 600;
236
+ }
237
+ return true;
238
+ }
239
+ function delay(ms) {
240
+ return new Promise((resolve) => setTimeout(resolve, ms));
241
+ }
242
+
243
+ // src/net/reconnect.ts
244
+ var Reconnector = class {
245
+ constructor(options) {
246
+ __publicField(this, "opts");
247
+ /** 已尝试的重连次数(用于退避指数与上限判断) */
248
+ __publicField(this, "attempts", 0);
249
+ /** 当前挂起的定时器 */
250
+ __publicField(this, "timer", null);
251
+ /** 是否已被取消(取消后所有 schedule 都无效) */
252
+ __publicField(this, "cancelled", false);
253
+ this.opts = {
254
+ autoReconnect: options.autoReconnect,
255
+ maxReconnect: options.maxReconnect,
256
+ baseIntervalMs: options.baseIntervalMs,
257
+ maxIntervalMs: options.maxIntervalMs ?? 3e4,
258
+ attempt: options.attempt,
259
+ onScheduled: options.onScheduled,
260
+ onGiveUp: options.onGiveUp
261
+ };
262
+ }
263
+ /** 连接成功后调用:清零计数,使下次断线从最短间隔重连 */
264
+ reset() {
265
+ this.attempts = 0;
266
+ }
267
+ /**
268
+ * 安排下一次重连。
269
+ * 若未启用自动重连、已取消、或已达到最大次数,则不再重连。
270
+ */
271
+ schedule() {
272
+ if (this.cancelled || !this.opts.autoReconnect) return;
273
+ if (this.attempts >= this.opts.maxReconnect) {
274
+ this.opts.onGiveUp?.();
275
+ return;
276
+ }
277
+ if (this.timer !== null) return;
278
+ const index = this.attempts;
279
+ const delay2 = this.computeDelay(index);
280
+ this.opts.onScheduled?.(index, delay2);
281
+ this.timer = setTimeout(() => {
282
+ this.timer = null;
283
+ if (this.cancelled) return;
284
+ this.attempts += 1;
285
+ this.opts.attempt().catch(() => {
286
+ if (!this.cancelled) this.schedule();
287
+ });
288
+ }, delay2);
289
+ }
290
+ /** 取消挂起的重连并永久禁用(主动关闭 / 销毁时调用) */
291
+ cancel() {
292
+ this.cancelled = true;
293
+ if (this.timer !== null) {
294
+ clearTimeout(this.timer);
295
+ this.timer = null;
296
+ }
297
+ }
298
+ /** 重新启用(主动关闭后又想复用同一实例时;通常配合 reset) */
299
+ revive() {
300
+ this.cancelled = false;
301
+ this.attempts = 0;
302
+ }
303
+ /** 计算第 index 次重连的退避间隔 */
304
+ computeDelay(index) {
305
+ const raw = this.opts.baseIntervalMs * Math.pow(2, index);
306
+ return Math.min(raw, this.opts.maxIntervalMs);
307
+ }
308
+ };
309
+
310
+ // src/net/WebSocketClient.ts
311
+ var WebSocketClient = class {
312
+ constructor(config) {
313
+ __publicField(this, "url");
314
+ __publicField(this, "heartbeatIntervalMs");
315
+ __publicField(this, "heartbeatPayload");
316
+ __publicField(this, "socket", null);
317
+ __publicField(this, "_state", "closed");
318
+ /** 当前底层 socket 上注册的取消订阅句柄,断开时统一清理 */
319
+ __publicField(this, "socketUnsubs", []);
320
+ /** 心跳定时器 */
321
+ __publicField(this, "heartbeatTimer", null);
322
+ /** 是否为主动关闭(主动关闭不重连) */
323
+ __publicField(this, "manualClose", false);
324
+ /** 当前 connect() 的 resolve/reject(用于把首次连接结果回传给调用方) */
325
+ __publicField(this, "pendingConnect", null);
326
+ __publicField(this, "reconnector");
327
+ // 业务侧事件监听集合
328
+ __publicField(this, "openListeners", /* @__PURE__ */ new Set());
329
+ __publicField(this, "messageListeners", /* @__PURE__ */ new Set());
330
+ __publicField(this, "errorListeners", /* @__PURE__ */ new Set());
331
+ __publicField(this, "closeListeners", /* @__PURE__ */ new Set());
332
+ this.url = config.url;
333
+ this.heartbeatIntervalMs = config.heartbeatIntervalMs ?? 0;
334
+ this.heartbeatPayload = config.heartbeatPayload ?? "ping";
335
+ this.reconnector = new Reconnector({
336
+ autoReconnect: config.autoReconnect ?? true,
337
+ maxReconnect: config.maxReconnect ?? Infinity,
338
+ baseIntervalMs: config.reconnectIntervalMs ?? 1e3,
339
+ // 重连尝试:重新打开底层 socket;成功(open)由事件回调触发 reset
340
+ attempt: () => this.openSocket()
341
+ });
342
+ }
343
+ get state() {
344
+ return this._state;
345
+ }
346
+ /** 建立连接:open 时 resolve,失败时 reject。重连由内部自动处理。 */
347
+ connect() {
348
+ if (this._state === "open") return Promise.resolve();
349
+ this.manualClose = false;
350
+ this.reconnector.revive();
351
+ return new Promise((resolve, reject) => {
352
+ this.pendingConnect = { resolve, reject };
353
+ this.openSocket().catch((e) => {
354
+ if (this.pendingConnect) {
355
+ this.pendingConnect.reject(e instanceof Error ? e : new Error(String(e)));
356
+ this.pendingConnect = null;
357
+ }
358
+ if (!this.manualClose) this.reconnector.schedule();
359
+ });
360
+ });
361
+ }
362
+ send(data) {
363
+ if (this._state !== "open" || !this.socket) {
364
+ throw new Error("[WebSocketClient] \u8FDE\u63A5\u672A\u5C31\u7EEA,\u65E0\u6CD5\u53D1\u9001");
365
+ }
366
+ this.socket.send(data);
367
+ }
368
+ /** 主动关闭:停心跳、取消重连、关闭底层 socket */
369
+ close() {
370
+ this.manualClose = true;
371
+ this.reconnector.cancel();
372
+ this.stopHeartbeat();
373
+ if (this.socket && (this._state === "open" || this._state === "connecting")) {
374
+ this._state = "closing";
375
+ this.socket.close();
376
+ } else {
377
+ this.setClosed();
378
+ }
379
+ }
380
+ onOpen(cb) {
381
+ this.openListeners.add(cb);
382
+ return () => this.openListeners.delete(cb);
383
+ }
384
+ onMessage(cb) {
385
+ this.messageListeners.add(cb);
386
+ return () => this.messageListeners.delete(cb);
387
+ }
388
+ onError(cb) {
389
+ this.errorListeners.add(cb);
390
+ return () => this.errorListeners.delete(cb);
391
+ }
392
+ onClose(cb) {
393
+ this.closeListeners.add(cb);
394
+ return () => this.closeListeners.delete(cb);
395
+ }
396
+ /* --------------------------- 内部实现 --------------------------- */
397
+ /** 打开一条底层 socket 并接线事件;返回的 Promise 在 open 时 resolve */
398
+ openSocket() {
399
+ this.teardownSocket();
400
+ this._state = "connecting";
401
+ const options = {
402
+ url: this.url,
403
+ binaryType: "arraybuffer"
404
+ };
405
+ return new Promise((resolve, reject) => {
406
+ let socket;
407
+ try {
408
+ socket = PlatformContext.current.net.createSocket(options);
409
+ } catch (e) {
410
+ this._state = "closed";
411
+ reject(e instanceof Error ? e : new Error(String(e)));
412
+ return;
413
+ }
414
+ this.socket = socket;
415
+ let settled = false;
416
+ this.socketUnsubs.push(
417
+ socket.onOpen(() => {
418
+ settled = true;
419
+ this._state = "open";
420
+ this.reconnector.reset();
421
+ this.startHeartbeat();
422
+ if (this.pendingConnect) {
423
+ this.pendingConnect.resolve();
424
+ this.pendingConnect = null;
425
+ }
426
+ this.emitOpen();
427
+ resolve();
428
+ })
429
+ );
430
+ this.socketUnsubs.push(
431
+ socket.onMessage((data) => this.emitMessage(data))
432
+ );
433
+ this.socketUnsubs.push(
434
+ socket.onError((err2) => {
435
+ this.emitError(err2);
436
+ if (!settled) {
437
+ settled = true;
438
+ reject(err2);
439
+ }
440
+ })
441
+ );
442
+ this.socketUnsubs.push(
443
+ socket.onClose(() => this.handleClose())
444
+ );
445
+ });
446
+ }
447
+ /** 底层 close 事件处理:停心跳、回调,非主动关闭则安排重连 */
448
+ handleClose() {
449
+ this.stopHeartbeat();
450
+ this.teardownSocket();
451
+ if (this.manualClose) {
452
+ this.setClosed();
453
+ return;
454
+ }
455
+ this._state = "closed";
456
+ this.emitClose();
457
+ this.reconnector.schedule();
458
+ }
459
+ /** 标记彻底关闭并通知业务 */
460
+ setClosed() {
461
+ if (this._state === "closed") return;
462
+ this._state = "closed";
463
+ this.emitClose();
464
+ }
465
+ /** 解绑当前 socket 的所有事件订阅 */
466
+ teardownSocket() {
467
+ for (const unsub of this.socketUnsubs) {
468
+ try {
469
+ unsub();
470
+ } catch {
471
+ }
472
+ }
473
+ this.socketUnsubs = [];
474
+ this.socket = null;
475
+ }
476
+ startHeartbeat() {
477
+ if (this.heartbeatIntervalMs <= 0) return;
478
+ this.stopHeartbeat();
479
+ this.heartbeatTimer = setInterval(() => {
480
+ if (this._state === "open" && this.socket) {
481
+ try {
482
+ this.socket.send(this.heartbeatPayload);
483
+ } catch {
484
+ }
485
+ }
486
+ }, this.heartbeatIntervalMs);
487
+ }
488
+ stopHeartbeat() {
489
+ if (this.heartbeatTimer !== null) {
490
+ clearInterval(this.heartbeatTimer);
491
+ this.heartbeatTimer = null;
492
+ }
493
+ }
494
+ emitOpen() {
495
+ for (const cb of this.openListeners) cb();
496
+ }
497
+ emitMessage(data) {
498
+ for (const cb of this.messageListeners) cb(data);
499
+ }
500
+ emitError(err2) {
501
+ for (const cb of this.errorListeners) cb(err2);
502
+ }
503
+ emitClose() {
504
+ for (const cb of this.closeListeners) cb();
505
+ }
506
+ };
507
+
508
+ // src/net/kcp/ikcp.ts
509
+ var IKCP_RTO_NDL = 30;
510
+ var IKCP_RTO_MIN = 100;
511
+ var IKCP_RTO_DEF = 200;
512
+ var IKCP_RTO_MAX = 6e4;
513
+ var IKCP_CMD_PUSH = 81;
514
+ var IKCP_CMD_ACK = 82;
515
+ var IKCP_CMD_WASK = 83;
516
+ var IKCP_CMD_WINS = 84;
517
+ var IKCP_ASK_SEND = 1;
518
+ var IKCP_ASK_TELL = 2;
519
+ var IKCP_WND_SND = 32;
520
+ var IKCP_WND_RCV = 128;
521
+ var IKCP_MTU_DEF = 1400;
522
+ var IKCP_INTERVAL = 100;
523
+ var IKCP_OVERHEAD = 24;
524
+ var IKCP_DEADLINK = 20;
525
+ var IKCP_THRESH_INIT = 2;
526
+ var IKCP_THRESH_MIN = 2;
527
+ var IKCP_PROBE_INIT = 7e3;
528
+ var IKCP_PROBE_LIMIT = 12e4;
529
+ var IKCP_FASTACK_LIMIT = 5;
530
+ function u32(x) {
531
+ return x >>> 0;
532
+ }
533
+ function _itimediff(later, earlier) {
534
+ return u32(later) - u32(earlier) | 0;
535
+ }
536
+ function _ibound(lower, middle, upper) {
537
+ return Math.min(Math.max(lower, middle), upper);
538
+ }
539
+ var IKCPSEG = class {
540
+ constructor(size = 0) {
541
+ __publicField(this, "conv", 0);
542
+ // 会话编号
543
+ __publicField(this, "cmd", 0);
544
+ // 命令字
545
+ __publicField(this, "frg", 0);
546
+ // 分片序号(倒序:0 表示最后一片)
547
+ __publicField(this, "wnd", 0);
548
+ // 发送方剩余接收窗口
549
+ __publicField(this, "ts", 0);
550
+ // 时间戳
551
+ __publicField(this, "sn", 0);
552
+ // 序号
553
+ __publicField(this, "una", 0);
554
+ // 待接收序号(对端已收到的连续最大 sn + 1)
555
+ __publicField(this, "len", 0);
556
+ // data 长度
557
+ // —— 发送缓冲控制字段(仅 snd_buf 中的段使用)——
558
+ __publicField(this, "resendts", 0);
559
+ // 下次重传时间戳
560
+ __publicField(this, "rto", 0);
561
+ // 该段的重传超时
562
+ __publicField(this, "fastack", 0);
563
+ // 被跨越次数(快速重传计数)
564
+ __publicField(this, "xmit", 0);
565
+ // 已发送次数
566
+ /** 段数据。固定持有一个 Uint8Array(可能长度为 0) */
567
+ __publicField(this, "data");
568
+ this.data = new Uint8Array(size);
569
+ }
570
+ };
571
+ function encode8u(view, offset, value) {
572
+ view.setUint8(offset, value & 255);
573
+ return offset + 1;
574
+ }
575
+ function encode16u(view, offset, value) {
576
+ view.setUint16(offset, value & 65535, true);
577
+ return offset + 2;
578
+ }
579
+ function encode32u(view, offset, value) {
580
+ view.setUint32(offset, u32(value), true);
581
+ return offset + 4;
582
+ }
583
+ function decode8u(view, offset) {
584
+ return [view.getUint8(offset), offset + 1];
585
+ }
586
+ function decode16u(view, offset) {
587
+ return [view.getUint16(offset, true), offset + 2];
588
+ }
589
+ function decode32u(view, offset) {
590
+ return [view.getUint32(offset, true), offset + 4];
591
+ }
592
+ var IKCPCB = class _IKCPCB {
593
+ constructor(conv, user) {
594
+ __publicField(this, "conv");
595
+ // 会话号
596
+ __publicField(this, "mtu");
597
+ // 最大传输单元
598
+ __publicField(this, "mss");
599
+ // 最大分片大小 = mtu - overhead
600
+ __publicField(this, "state", 0);
601
+ // 连接状态(0 正常,-1 断链)
602
+ __publicField(this, "snd_una", 0);
603
+ // 第一个未确认的包
604
+ __publicField(this, "snd_nxt", 0);
605
+ // 下一个待分配序号
606
+ __publicField(this, "rcv_nxt", 0);
607
+ // 待接收的下一个序号
608
+ __publicField(this, "ts_recent", 0);
609
+ __publicField(this, "ts_lastack", 0);
610
+ __publicField(this, "ssthresh", IKCP_THRESH_INIT);
611
+ // 拥塞窗口阈值
612
+ __publicField(this, "rx_rttval", 0);
613
+ // RTT 平均偏差
614
+ __publicField(this, "rx_srtt", 0);
615
+ // 平滑后 RTT
616
+ __publicField(this, "rx_rto", IKCP_RTO_DEF);
617
+ // 重传超时
618
+ __publicField(this, "rx_minrto", IKCP_RTO_MIN);
619
+ // 最小 RTO
620
+ __publicField(this, "snd_wnd", IKCP_WND_SND);
621
+ // 发送窗口
622
+ __publicField(this, "rcv_wnd", IKCP_WND_RCV);
623
+ // 接收窗口
624
+ __publicField(this, "rmt_wnd", IKCP_WND_RCV);
625
+ // 远端接收窗口
626
+ __publicField(this, "cwnd", 0);
627
+ // 拥塞窗口
628
+ __publicField(this, "probe", 0);
629
+ // 探测标志(ASK_SEND / ASK_TELL)
630
+ __publicField(this, "current", 0);
631
+ // 当前时间(update 传入)
632
+ __publicField(this, "interval", IKCP_INTERVAL);
633
+ // flush 间隔
634
+ __publicField(this, "ts_flush", IKCP_INTERVAL);
635
+ // 下次 flush 时间
636
+ __publicField(this, "xmit", 0);
637
+ // 总重传次数
638
+ __publicField(this, "nrcv_buf", 0);
639
+ // rcv_buf 段数
640
+ __publicField(this, "nsnd_buf", 0);
641
+ // snd_buf 段数
642
+ __publicField(this, "nrcv_que", 0);
643
+ // rcv_queue 段数
644
+ __publicField(this, "nsnd_que", 0);
645
+ // snd_queue 段数
646
+ // 注意:ikcp.c 里字段与函数同名都叫 nodelay;TS 不允许字段与方法同名,
647
+ // 这里字段重命名为 nodelayMode,配置方法仍叫 nodelay()。
648
+ __publicField(this, "nodelayMode", 0);
649
+ // 是否 nodelay 模式(0 关闭,1/2 启用)
650
+ __publicField(this, "updated", 0);
651
+ // 是否调用过 update
652
+ __publicField(this, "ts_probe", 0);
653
+ // 下次探测窗口时间
654
+ __publicField(this, "probe_wait", 0);
655
+ // 探测等待时长
656
+ __publicField(this, "dead_link", IKCP_DEADLINK);
657
+ // 段最大重传次数(超过判定断链)
658
+ __publicField(this, "incr", 0);
659
+ // 拥塞窗口字节增量
660
+ // —— 四个核心队列 / 缓冲(对应 ikcp.c 的链表,这里用数组)——
661
+ __publicField(this, "snd_queue", []);
662
+ // 待发送(尚未进入发送窗口)
663
+ __publicField(this, "rcv_queue", []);
664
+ // 已就绪有序、待 recv 取走
665
+ __publicField(this, "snd_buf", []);
666
+ // 已发送、等待 ACK
667
+ __publicField(this, "rcv_buf", []);
668
+ // 已收到、等待重组(可能乱序)
669
+ // ack 列表:成对存放 (sn, ts),flush 时打包成 ACK 包发回
670
+ __publicField(this, "acklist", []);
671
+ __publicField(this, "ackcount", 0);
672
+ __publicField(this, "fastresend", 0);
673
+ // 快速重传触发阈值(跨越次数)
674
+ __publicField(this, "fastlimit", IKCP_FASTACK_LIMIT);
675
+ // 快速重传最大次数
676
+ __publicField(this, "nocwnd", 0);
677
+ // 是否关闭拥塞控制
678
+ __publicField(this, "stream", 0);
679
+ // 流模式(0 消息模式)
680
+ __publicField(this, "user");
681
+ // 透传给 output 的用户数据
682
+ __publicField(this, "output", null);
683
+ // flush 时复用的缓冲区,容量 (mtu + overhead) 足够放一个 MTU 包
684
+ __publicField(this, "buffer");
685
+ this.conv = u32(conv);
686
+ this.user = user;
687
+ this.mtu = IKCP_MTU_DEF;
688
+ this.mss = this.mtu - IKCP_OVERHEAD;
689
+ this.buffer = new Uint8Array((this.mtu + IKCP_OVERHEAD) * 3);
690
+ }
691
+ /* ----------------------- 创建 / 释放 / output ----------------------- */
692
+ /** 对应 ikcp_create:工厂方法 */
693
+ static create(conv, user) {
694
+ return new _IKCPCB(conv, user);
695
+ }
696
+ /** 对应 ikcp_release:释放资源(JS 下置空引用即可) */
697
+ release() {
698
+ this.snd_queue = [];
699
+ this.rcv_queue = [];
700
+ this.snd_buf = [];
701
+ this.rcv_buf = [];
702
+ this.acklist = [];
703
+ this.output = null;
704
+ this.nrcv_buf = this.nsnd_buf = this.nrcv_que = this.nsnd_que = 0;
705
+ this.ackcount = 0;
706
+ }
707
+ /** 对应 ikcp_setoutput:设置底层发送回调 */
708
+ setoutput(output) {
709
+ this.output = output;
710
+ }
711
+ /* ----------------------------- recv ----------------------------- */
712
+ /**
713
+ * 对应 ikcp_recv:从 rcv_queue 取出一条完整的应用层消息。
714
+ * 返回拼接好的 Uint8Array;无完整消息时返回 null。
715
+ *
716
+ * 调用方一般循环 recv 直到返回 null,把所有就绪消息取空。
717
+ */
718
+ recv() {
719
+ const peeksize = this.peeksize();
720
+ if (peeksize < 0) return null;
721
+ const recover = this.nrcv_que >= this.rcv_wnd;
722
+ const out = new Uint8Array(peeksize);
723
+ let n = 0;
724
+ let count = 0;
725
+ for (const seg of this.rcv_queue) {
726
+ out.set(seg.data, n);
727
+ n += seg.len;
728
+ count++;
729
+ if (seg.frg === 0) break;
730
+ }
731
+ if (count > 0) {
732
+ this.rcv_queue.splice(0, count);
733
+ this.nrcv_que -= count;
734
+ }
735
+ this._moveBufToQueue();
736
+ if (this.nrcv_que < this.rcv_wnd && recover) {
737
+ this.probe |= IKCP_ASK_TELL;
738
+ }
739
+ return out;
740
+ }
741
+ /**
742
+ * 对应 ikcp_peeksize:返回下一条完整消息的总字节数;无完整消息返回 -1。
743
+ */
744
+ peeksize() {
745
+ if (this.rcv_queue.length === 0) return -1;
746
+ const first = this.rcv_queue[0];
747
+ if (first.frg === 0) return first.len;
748
+ if (this.nrcv_que < first.frg + 1) return -1;
749
+ let length = 0;
750
+ for (const seg of this.rcv_queue) {
751
+ length += seg.len;
752
+ if (seg.frg === 0) break;
753
+ }
754
+ return length;
755
+ }
756
+ /* ----------------------------- send ----------------------------- */
757
+ /**
758
+ * 对应 ikcp_send:把上层数据切成若干 <= mss 的分片放进 snd_queue。
759
+ * 返回 0 成功,负值失败。
760
+ *
761
+ * 这里实现消息模式(stream=0):每次 send 是一条独立消息,frg 倒序标注分片。
762
+ * 同时实现 stream 模式的合并(stream=1 时尝试把数据追加到上一段)。
763
+ */
764
+ send(buffer) {
765
+ if (buffer.length === 0) return -1;
766
+ let offset = 0;
767
+ let len = buffer.length;
768
+ if (this.stream !== 0 && this.snd_queue.length > 0) {
769
+ const old = this.snd_queue[this.snd_queue.length - 1];
770
+ if (old.len < this.mss) {
771
+ const capacity = this.mss - old.len;
772
+ const extend = Math.min(len, capacity);
773
+ const merged = new Uint8Array(old.len + extend);
774
+ merged.set(old.data.subarray(0, old.len), 0);
775
+ merged.set(buffer.subarray(offset, offset + extend), old.len);
776
+ old.data = merged;
777
+ old.len = merged.length;
778
+ old.frg = 0;
779
+ offset += extend;
780
+ len -= extend;
781
+ }
782
+ if (len === 0) return 0;
783
+ }
784
+ let count;
785
+ if (len <= this.mss) count = 1;
786
+ else count = Math.floor((len + this.mss - 1) / this.mss);
787
+ if (count >= IKCP_WND_RCV) return -2;
788
+ if (count === 0) count = 1;
789
+ for (let i = 0; i < count; i++) {
790
+ const size = len > this.mss ? this.mss : len;
791
+ const seg = new IKCPSEG(size);
792
+ if (size > 0) seg.data.set(buffer.subarray(offset, offset + size));
793
+ seg.len = size;
794
+ seg.frg = this.stream === 0 ? count - i - 1 : 0;
795
+ this.snd_queue.push(seg);
796
+ this.nsnd_que++;
797
+ offset += size;
798
+ len -= size;
799
+ }
800
+ return 0;
801
+ }
802
+ /* --------------------------- RTT / RTO --------------------------- */
803
+ /** 对应 ikcp_update_ack:基于一次 RTT 样本更新 srtt / rttval / rto */
804
+ update_ack(rtt) {
805
+ if (this.rx_srtt === 0) {
806
+ this.rx_srtt = rtt;
807
+ this.rx_rttval = rtt >> 1;
808
+ } else {
809
+ let delta = rtt - this.rx_srtt;
810
+ if (delta < 0) delta = -delta;
811
+ this.rx_rttval = 3 * this.rx_rttval + delta >> 2;
812
+ this.rx_srtt = 7 * this.rx_srtt + rtt >> 3;
813
+ if (this.rx_srtt < 1) this.rx_srtt = 1;
814
+ }
815
+ const rto = this.rx_srtt + Math.max(this.interval, 4 * this.rx_rttval);
816
+ this.rx_rto = _ibound(this.rx_minrto, rto, IKCP_RTO_MAX);
817
+ }
818
+ /** 对应 ikcp_shrink_buf:根据 snd_buf 首段更新 snd_una */
819
+ shrink_buf() {
820
+ if (this.snd_buf.length > 0) {
821
+ this.snd_una = this.snd_buf[0].sn;
822
+ } else {
823
+ this.snd_una = this.snd_nxt;
824
+ }
825
+ }
826
+ /** 对应 ikcp_parse_ack:收到对端 ACK,移除 snd_buf 中已确认的段 */
827
+ parse_ack(sn) {
828
+ if (_itimediff(sn, this.snd_una) < 0 || _itimediff(sn, this.snd_nxt) >= 0) {
829
+ return;
830
+ }
831
+ for (let i = 0; i < this.snd_buf.length; i++) {
832
+ const seg = this.snd_buf[i];
833
+ if (sn === seg.sn) {
834
+ this.snd_buf.splice(i, 1);
835
+ this.nsnd_buf--;
836
+ break;
837
+ }
838
+ if (_itimediff(sn, seg.sn) < 0) break;
839
+ }
840
+ }
841
+ /** 对应 ikcp_parse_una:收到 una,移除所有 sn < una 的已确认段 */
842
+ parse_una(una) {
843
+ let count = 0;
844
+ for (const seg of this.snd_buf) {
845
+ if (_itimediff(una, seg.sn) > 0) count++;
846
+ else break;
847
+ }
848
+ if (count > 0) {
849
+ this.snd_buf.splice(0, count);
850
+ this.nsnd_buf -= count;
851
+ }
852
+ }
853
+ /** 对应 ikcp_parse_fastack:统计被跨越次数,用于快速重传 */
854
+ parse_fastack(sn, ts) {
855
+ if (_itimediff(sn, this.snd_una) < 0 || _itimediff(sn, this.snd_nxt) >= 0) {
856
+ return;
857
+ }
858
+ for (const seg of this.snd_buf) {
859
+ if (_itimediff(sn, seg.sn) < 0) break;
860
+ else if (sn !== seg.sn) {
861
+ if (_itimediff(ts, seg.ts) >= 0) {
862
+ seg.fastack++;
863
+ } else {
864
+ seg.fastack++;
865
+ }
866
+ }
867
+ }
868
+ }
869
+ /* ----------------------------- ack 列表 ----------------------------- */
870
+ /** 对应 ikcp_ack_push:暂存一个待回的 ACK(sn,ts) */
871
+ ack_push(sn, ts) {
872
+ this.acklist.push(sn, ts);
873
+ this.ackcount++;
874
+ }
875
+ /** 对应 ikcp_ack_get:取出第 p 个待回 ACK 的 (sn, ts) */
876
+ ack_get(p) {
877
+ return [this.acklist[p * 2], this.acklist[p * 2 + 1]];
878
+ }
879
+ /* ----------------------------- 接收缓冲 ----------------------------- */
880
+ /** 对应 ikcp_parse_data:把一个新到的 PUSH 段插入 rcv_buf(去重 + 有序插入) */
881
+ parse_data(newseg) {
882
+ const sn = newseg.sn;
883
+ if (_itimediff(sn, this.rcv_nxt + this.rcv_wnd) >= 0 || _itimediff(sn, this.rcv_nxt) < 0) {
884
+ return;
885
+ }
886
+ let insertPos = this.rcv_buf.length;
887
+ let repeat = false;
888
+ for (let i = this.rcv_buf.length - 1; i >= 0; i--) {
889
+ const seg = this.rcv_buf[i];
890
+ if (seg.sn === sn) {
891
+ repeat = true;
892
+ break;
893
+ }
894
+ if (_itimediff(sn, seg.sn) > 0) {
895
+ insertPos = i + 1;
896
+ break;
897
+ }
898
+ insertPos = i;
899
+ }
900
+ if (!repeat) {
901
+ this.rcv_buf.splice(insertPos, 0, newseg);
902
+ this.nrcv_buf++;
903
+ }
904
+ this._moveBufToQueue();
905
+ }
906
+ /** 把 rcv_buf 中 sn == rcv_nxt 的连续段顺次搬进 rcv_queue */
907
+ _moveBufToQueue() {
908
+ let moved = 0;
909
+ for (const seg of this.rcv_buf) {
910
+ if (seg.sn === this.rcv_nxt && this.nrcv_que < this.rcv_wnd) {
911
+ this.rcv_queue.push(seg);
912
+ this.nrcv_que++;
913
+ this.rcv_nxt = u32(this.rcv_nxt + 1);
914
+ moved++;
915
+ } else {
916
+ break;
917
+ }
918
+ }
919
+ if (moved > 0) {
920
+ this.rcv_buf.splice(0, moved);
921
+ this.nrcv_buf -= moved;
922
+ }
923
+ }
924
+ /* ----------------------------- input ----------------------------- */
925
+ /**
926
+ * 对应 ikcp_input:解析底层收到的一个或多个 KCP 包(可能粘在一起)。
927
+ * 返回 0 成功,负值表示数据非法。
928
+ */
929
+ input(data) {
930
+ const prev_una = this.snd_una;
931
+ let maxack = 0;
932
+ let latest_ts = 0;
933
+ let flag = 0;
934
+ if (data.length < IKCP_OVERHEAD) return -1;
935
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
936
+ let offset = 0;
937
+ while (true) {
938
+ if (data.length - offset < IKCP_OVERHEAD) break;
939
+ let conv, cmd, frg, wnd;
940
+ let ts, sn, una, len;
941
+ [conv, offset] = decode32u(view, offset);
942
+ if (conv !== this.conv) return -1;
943
+ [cmd, offset] = decode8u(view, offset);
944
+ [frg, offset] = decode8u(view, offset);
945
+ [wnd, offset] = decode16u(view, offset);
946
+ [ts, offset] = decode32u(view, offset);
947
+ [sn, offset] = decode32u(view, offset);
948
+ [una, offset] = decode32u(view, offset);
949
+ [len, offset] = decode32u(view, offset);
950
+ if (data.length - offset < len) return -2;
951
+ if (cmd !== IKCP_CMD_PUSH && cmd !== IKCP_CMD_ACK && cmd !== IKCP_CMD_WASK && cmd !== IKCP_CMD_WINS) {
952
+ return -3;
953
+ }
954
+ this.rmt_wnd = wnd;
955
+ this.parse_una(una);
956
+ this.shrink_buf();
957
+ if (cmd === IKCP_CMD_ACK) {
958
+ if (_itimediff(this.current, ts) >= 0) {
959
+ this.update_ack(_itimediff(this.current, ts));
960
+ }
961
+ this.parse_ack(sn);
962
+ this.shrink_buf();
963
+ if (flag === 0) {
964
+ flag = 1;
965
+ maxack = sn;
966
+ latest_ts = ts;
967
+ } else if (_itimediff(sn, maxack) > 0) {
968
+ maxack = sn;
969
+ latest_ts = ts;
970
+ }
971
+ } else if (cmd === IKCP_CMD_PUSH) {
972
+ if (_itimediff(sn, this.rcv_nxt + this.rcv_wnd) < 0) {
973
+ this.ack_push(sn, ts);
974
+ if (_itimediff(sn, this.rcv_nxt) >= 0) {
975
+ const seg = new IKCPSEG(len);
976
+ seg.conv = conv;
977
+ seg.cmd = cmd;
978
+ seg.frg = frg;
979
+ seg.wnd = wnd;
980
+ seg.ts = ts;
981
+ seg.sn = sn;
982
+ seg.una = una;
983
+ seg.len = len;
984
+ if (len > 0) {
985
+ seg.data.set(data.subarray(offset, offset + len));
986
+ }
987
+ this.parse_data(seg);
988
+ }
989
+ }
990
+ } else if (cmd === IKCP_CMD_WASK) {
991
+ this.probe |= IKCP_ASK_TELL;
992
+ } else ;
993
+ offset += len;
994
+ }
995
+ if (flag !== 0) {
996
+ this.parse_fastack(maxack, latest_ts);
997
+ }
998
+ if (_itimediff(this.snd_una, prev_una) > 0) {
999
+ if (this.cwnd < this.rmt_wnd) {
1000
+ const mss = this.mss;
1001
+ if (this.cwnd < this.ssthresh) {
1002
+ this.cwnd++;
1003
+ this.incr += mss;
1004
+ } else {
1005
+ if (this.incr < mss) this.incr = mss;
1006
+ this.incr += Math.floor(mss * mss / this.incr) + Math.floor(mss / 16);
1007
+ if ((this.cwnd + 1) * mss <= this.incr) {
1008
+ this.cwnd = Math.floor((this.incr + mss - 1) / (mss > 0 ? mss : 1));
1009
+ }
1010
+ }
1011
+ if (this.cwnd > this.rmt_wnd) {
1012
+ this.cwnd = this.rmt_wnd;
1013
+ this.incr = this.rmt_wnd * mss;
1014
+ }
1015
+ }
1016
+ }
1017
+ return 0;
1018
+ }
1019
+ /* ----------------------------- 窗口 ----------------------------- */
1020
+ /** 对应 ikcp_wnd_unused:本端剩余可用接收窗口 */
1021
+ wnd_unused() {
1022
+ if (this.nrcv_que < this.rcv_wnd) {
1023
+ return this.rcv_wnd - this.nrcv_que;
1024
+ }
1025
+ return 0;
1026
+ }
1027
+ /* ----------------------------- flush ----------------------------- */
1028
+ /**
1029
+ * 对应 ikcp_flush:把 ack、窗口探测、待发数据、需重传段统统编码发出。
1030
+ * 这是 KCP 真正"产生网络包"的地方,由 update 周期性调用。
1031
+ */
1032
+ flush() {
1033
+ if (this.updated === 0) return;
1034
+ const current = this.current;
1035
+ const buffer = this.buffer;
1036
+ const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
1037
+ let offset = 0;
1038
+ const seg = new IKCPSEG(0);
1039
+ seg.conv = this.conv;
1040
+ seg.cmd = IKCP_CMD_ACK;
1041
+ seg.frg = 0;
1042
+ seg.wnd = this.wnd_unused();
1043
+ seg.una = this.rcv_nxt;
1044
+ seg.sn = 0;
1045
+ seg.ts = 0;
1046
+ seg.len = 0;
1047
+ const flushBuffer = () => {
1048
+ if (offset > 0) {
1049
+ this.output?.(buffer.subarray(0, offset), this);
1050
+ offset = 0;
1051
+ }
1052
+ };
1053
+ const makeSpace = (need) => {
1054
+ if (offset + need > this.mtu) flushBuffer();
1055
+ };
1056
+ for (let i = 0; i < this.ackcount; i++) {
1057
+ makeSpace(IKCP_OVERHEAD);
1058
+ const [sn, ts] = this.ack_get(i);
1059
+ seg.sn = sn;
1060
+ seg.ts = ts;
1061
+ offset = this.encodeSeg(view, offset, seg);
1062
+ }
1063
+ this.ackcount = 0;
1064
+ this.acklist = [];
1065
+ if (this.rmt_wnd === 0) {
1066
+ if (this.probe_wait === 0) {
1067
+ this.probe_wait = IKCP_PROBE_INIT;
1068
+ this.ts_probe = u32(this.current + this.probe_wait);
1069
+ } else {
1070
+ if (_itimediff(this.current, this.ts_probe) >= 0) {
1071
+ if (this.probe_wait < IKCP_PROBE_INIT) this.probe_wait = IKCP_PROBE_INIT;
1072
+ this.probe_wait += this.probe_wait >> 1;
1073
+ if (this.probe_wait > IKCP_PROBE_LIMIT) this.probe_wait = IKCP_PROBE_LIMIT;
1074
+ this.ts_probe = u32(this.current + this.probe_wait);
1075
+ this.probe |= IKCP_ASK_SEND;
1076
+ }
1077
+ }
1078
+ } else {
1079
+ this.ts_probe = 0;
1080
+ this.probe_wait = 0;
1081
+ }
1082
+ if (this.probe & IKCP_ASK_SEND) {
1083
+ seg.cmd = IKCP_CMD_WASK;
1084
+ makeSpace(IKCP_OVERHEAD);
1085
+ seg.sn = 0;
1086
+ seg.ts = 0;
1087
+ offset = this.encodeSeg(view, offset, seg);
1088
+ }
1089
+ if (this.probe & IKCP_ASK_TELL) {
1090
+ seg.cmd = IKCP_CMD_WINS;
1091
+ makeSpace(IKCP_OVERHEAD);
1092
+ seg.sn = 0;
1093
+ seg.ts = 0;
1094
+ offset = this.encodeSeg(view, offset, seg);
1095
+ }
1096
+ this.probe = 0;
1097
+ let cwnd = Math.min(this.snd_wnd, this.rmt_wnd);
1098
+ if (this.nocwnd === 0) cwnd = Math.min(this.cwnd, cwnd);
1099
+ while (_itimediff(this.snd_nxt, u32(this.snd_una + cwnd)) < 0) {
1100
+ if (this.snd_queue.length === 0) break;
1101
+ const newseg = this.snd_queue.shift();
1102
+ this.nsnd_que--;
1103
+ newseg.conv = this.conv;
1104
+ newseg.cmd = IKCP_CMD_PUSH;
1105
+ newseg.wnd = seg.wnd;
1106
+ newseg.ts = current;
1107
+ newseg.sn = this.snd_nxt;
1108
+ this.snd_nxt = u32(this.snd_nxt + 1);
1109
+ newseg.una = this.rcv_nxt;
1110
+ newseg.resendts = current;
1111
+ newseg.rto = this.rx_rto;
1112
+ newseg.fastack = 0;
1113
+ newseg.xmit = 0;
1114
+ this.snd_buf.push(newseg);
1115
+ this.nsnd_buf++;
1116
+ }
1117
+ const resent = this.fastresend > 0 ? this.fastresend : 4294967295;
1118
+ const rtomin = this.nodelayMode === 0 ? this.rx_rto >> 3 : 0;
1119
+ let change = 0;
1120
+ let lost = 0;
1121
+ for (const segment of this.snd_buf) {
1122
+ let needsend = false;
1123
+ if (segment.xmit === 0) {
1124
+ needsend = true;
1125
+ segment.xmit++;
1126
+ segment.rto = this.rx_rto;
1127
+ segment.resendts = u32(current + segment.rto + rtomin);
1128
+ } else if (_itimediff(current, segment.resendts) >= 0) {
1129
+ needsend = true;
1130
+ segment.xmit++;
1131
+ this.xmit++;
1132
+ if (this.nodelayMode === 0) {
1133
+ segment.rto += Math.max(segment.rto, this.rx_rto);
1134
+ } else {
1135
+ const step = this.nodelayMode < 2 ? segment.rto : this.rx_rto;
1136
+ segment.rto += step >> 1;
1137
+ }
1138
+ segment.resendts = u32(current + segment.rto);
1139
+ lost = 1;
1140
+ } else if (segment.fastack >= resent) {
1141
+ if (segment.xmit <= this.fastlimit || this.fastlimit <= 0) {
1142
+ needsend = true;
1143
+ segment.xmit++;
1144
+ segment.fastack = 0;
1145
+ segment.resendts = u32(current + segment.rto);
1146
+ change++;
1147
+ }
1148
+ }
1149
+ if (needsend) {
1150
+ segment.ts = current;
1151
+ segment.wnd = seg.wnd;
1152
+ segment.una = this.rcv_nxt;
1153
+ const need = IKCP_OVERHEAD + segment.len;
1154
+ makeSpace(need);
1155
+ offset = this.encodeSeg(view, offset, segment);
1156
+ if (segment.len > 0) {
1157
+ buffer.set(segment.data.subarray(0, segment.len), offset);
1158
+ offset += segment.len;
1159
+ }
1160
+ if (segment.xmit >= this.dead_link) {
1161
+ this.state = -1;
1162
+ }
1163
+ }
1164
+ }
1165
+ flushBuffer();
1166
+ if (change > 0) {
1167
+ const inflight = u32(this.snd_nxt - this.snd_una);
1168
+ this.ssthresh = inflight >> 1;
1169
+ if (this.ssthresh < IKCP_THRESH_MIN) this.ssthresh = IKCP_THRESH_MIN;
1170
+ this.cwnd = this.ssthresh + resent;
1171
+ this.incr = this.cwnd * this.mss;
1172
+ }
1173
+ if (lost !== 0) {
1174
+ this.ssthresh = cwnd >> 1;
1175
+ if (this.ssthresh < IKCP_THRESH_MIN) this.ssthresh = IKCP_THRESH_MIN;
1176
+ this.cwnd = 1;
1177
+ this.incr = this.mss;
1178
+ }
1179
+ if (this.cwnd < 1) {
1180
+ this.cwnd = 1;
1181
+ this.incr = this.mss;
1182
+ }
1183
+ }
1184
+ /** 把一个 SEG 的 24 字节包头写入 buffer,返回新 offset(不含 data) */
1185
+ encodeSeg(view, offset, seg) {
1186
+ offset = encode32u(view, offset, seg.conv);
1187
+ offset = encode8u(view, offset, seg.cmd);
1188
+ offset = encode8u(view, offset, seg.frg);
1189
+ offset = encode16u(view, offset, seg.wnd);
1190
+ offset = encode32u(view, offset, seg.ts);
1191
+ offset = encode32u(view, offset, seg.sn);
1192
+ offset = encode32u(view, offset, seg.una);
1193
+ offset = encode32u(view, offset, seg.len);
1194
+ return offset;
1195
+ }
1196
+ /* ----------------------------- update / check ----------------------------- */
1197
+ /**
1198
+ * 对应 ikcp_update:由调用方按 interval 周期性调用,内部到点触发 flush。
1199
+ * current 为当前毫秒时间戳。
1200
+ */
1201
+ update(current) {
1202
+ this.current = u32(current);
1203
+ if (this.updated === 0) {
1204
+ this.updated = 1;
1205
+ this.ts_flush = this.current;
1206
+ }
1207
+ let slap = _itimediff(this.current, this.ts_flush);
1208
+ if (slap >= 1e4 || slap < -1e4) {
1209
+ this.ts_flush = this.current;
1210
+ slap = 0;
1211
+ }
1212
+ if (slap >= 0) {
1213
+ this.ts_flush = u32(this.ts_flush + this.interval);
1214
+ if (_itimediff(this.current, this.ts_flush) >= 0) {
1215
+ this.ts_flush = u32(this.current + this.interval);
1216
+ }
1217
+ this.flush();
1218
+ }
1219
+ }
1220
+ /**
1221
+ * 对应 ikcp_check:返回下一次必须调用 update 的时间戳(ms)。
1222
+ * 调用方可据此节流:在该时刻之前无需频繁 update。
1223
+ */
1224
+ check(current) {
1225
+ const cur = u32(current);
1226
+ let ts_flush = this.ts_flush;
1227
+ let tm_flush = 2147483647;
1228
+ let tm_packet = 2147483647;
1229
+ if (this.updated === 0) return cur;
1230
+ if (_itimediff(cur, ts_flush) >= 1e4 || _itimediff(cur, ts_flush) < -1e4) {
1231
+ ts_flush = cur;
1232
+ }
1233
+ if (_itimediff(cur, ts_flush) >= 0) return cur;
1234
+ tm_flush = _itimediff(ts_flush, cur);
1235
+ for (const seg of this.snd_buf) {
1236
+ const diff = _itimediff(seg.resendts, cur);
1237
+ if (diff <= 0) return cur;
1238
+ if (diff < tm_packet) tm_packet = diff;
1239
+ }
1240
+ let minimal = tm_packet < tm_flush ? tm_packet : tm_flush;
1241
+ if (minimal >= this.interval) minimal = this.interval;
1242
+ return u32(cur + minimal);
1243
+ }
1244
+ /* ----------------------------- 配置 ----------------------------- */
1245
+ /** 对应 ikcp_wndsize:设置发送 / 接收窗口大小 */
1246
+ wndsize(sndwnd, rcvwnd) {
1247
+ if (sndwnd > 0) this.snd_wnd = sndwnd;
1248
+ if (rcvwnd > 0) this.rcv_wnd = Math.max(rcvwnd, IKCP_WND_RCV);
1249
+ return 0;
1250
+ }
1251
+ /** 对应 ikcp_waitsnd:返回待发送 + 未确认的段数(供上层判断积压) */
1252
+ waitsnd() {
1253
+ return this.nsnd_buf + this.nsnd_que;
1254
+ }
1255
+ /**
1256
+ * 对应 ikcp_nodelay:配置低延迟参数。
1257
+ * @param nodelay 0 关闭(默认),1 启用(rto 最小值降到 30ms)
1258
+ * @param interval flush 间隔(10~5000ms)
1259
+ * @param resend 快速重传跨越阈值(>0 启用)
1260
+ * @param nc 是否关闭拥塞控制(1 关闭)
1261
+ */
1262
+ nodelay(nodelay, interval, resend, nc) {
1263
+ if (nodelay >= 0) {
1264
+ this.nodelayMode = nodelay;
1265
+ this.rx_minrto = nodelay !== 0 ? IKCP_RTO_NDL : IKCP_RTO_MIN;
1266
+ }
1267
+ if (interval >= 0) {
1268
+ let it = interval;
1269
+ if (it > 5e3) it = 5e3;
1270
+ else if (it < 10) it = 10;
1271
+ this.interval = it;
1272
+ }
1273
+ if (resend >= 0) this.fastresend = resend;
1274
+ if (nc >= 0) this.nocwnd = nc;
1275
+ return 0;
1276
+ }
1277
+ /** 对应 ikcp_setmtu:设置 MTU 并重建发送缓冲 */
1278
+ setmtu(mtu) {
1279
+ if (mtu < 50 || mtu < IKCP_OVERHEAD) return -1;
1280
+ this.mtu = mtu;
1281
+ this.mss = this.mtu - IKCP_OVERHEAD;
1282
+ this.buffer = new Uint8Array((mtu + IKCP_OVERHEAD) * 3);
1283
+ return 0;
1284
+ }
1285
+ /** 对应 ikcp_setinterval:单独设置 flush 间隔 */
1286
+ setinterval(interval) {
1287
+ let it = interval;
1288
+ if (it > 5e3) it = 5e3;
1289
+ else if (it < 10) it = 10;
1290
+ this.interval = it;
1291
+ return 0;
1292
+ }
1293
+ /** 对应 ikcp_getconv:从一个数据报里读出 conv(无需实例) */
1294
+ static getconv(data) {
1295
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
1296
+ return view.getUint32(0, true);
1297
+ }
1298
+ };
1299
+
1300
+ // src/net/KcpClient.ts
1301
+ var KcpClient = class {
1302
+ constructor(config, transportFactory) {
1303
+ __publicField(this, "url");
1304
+ __publicField(this, "conv");
1305
+ __publicField(this, "intervalMs");
1306
+ __publicField(this, "cfgNodelay");
1307
+ __publicField(this, "cfgResend");
1308
+ __publicField(this, "cfgNc");
1309
+ __publicField(this, "cfgSndWnd");
1310
+ __publicField(this, "cfgRcvWnd");
1311
+ __publicField(this, "transportFactory");
1312
+ __publicField(this, "kcp", null);
1313
+ __publicField(this, "transport", null);
1314
+ __publicField(this, "updateTimer", null);
1315
+ __publicField(this, "_state", "closed");
1316
+ __publicField(this, "manualClose", false);
1317
+ __publicField(this, "transportUnsubs", []);
1318
+ __publicField(this, "pendingConnect", null);
1319
+ __publicField(this, "reconnector");
1320
+ __publicField(this, "textEncoder", new TextEncoder());
1321
+ __publicField(this, "openListeners", /* @__PURE__ */ new Set());
1322
+ __publicField(this, "messageListeners", /* @__PURE__ */ new Set());
1323
+ __publicField(this, "errorListeners", /* @__PURE__ */ new Set());
1324
+ __publicField(this, "closeListeners", /* @__PURE__ */ new Set());
1325
+ this.url = config.url;
1326
+ this.conv = config.conv;
1327
+ this.intervalMs = config.intervalMs ?? 10;
1328
+ this.cfgNodelay = config.nodelay ? 1 : 0;
1329
+ this.cfgResend = config.resend ?? 0;
1330
+ this.cfgNc = config.nc ? 1 : 0;
1331
+ this.cfgSndWnd = config.sndWnd;
1332
+ this.cfgRcvWnd = config.rcvWnd;
1333
+ this.transportFactory = transportFactory ?? ((url) => this.createDefaultTransport(url));
1334
+ this.reconnector = new Reconnector({
1335
+ autoReconnect: config.autoReconnect ?? true,
1336
+ maxReconnect: config.maxReconnect ?? Infinity,
1337
+ baseIntervalMs: config.reconnectIntervalMs ?? 1e3,
1338
+ attempt: () => this.openTransport()
1339
+ });
1340
+ }
1341
+ get state() {
1342
+ return this._state;
1343
+ }
1344
+ connect() {
1345
+ if (this._state === "open") return Promise.resolve();
1346
+ this.manualClose = false;
1347
+ this.reconnector.revive();
1348
+ return new Promise((resolve, reject) => {
1349
+ this.pendingConnect = { resolve, reject };
1350
+ this.openTransport().catch((e) => {
1351
+ if (this.pendingConnect) {
1352
+ this.pendingConnect.reject(e instanceof Error ? e : new Error(String(e)));
1353
+ this.pendingConnect = null;
1354
+ }
1355
+ if (!this.manualClose) this.reconnector.schedule();
1356
+ });
1357
+ });
1358
+ }
1359
+ send(data) {
1360
+ if (this._state !== "open" || !this.kcp) {
1361
+ throw new Error("[KcpClient] \u8FDE\u63A5\u672A\u5C31\u7EEA,\u65E0\u6CD5\u53D1\u9001");
1362
+ }
1363
+ const bytes = typeof data === "string" ? this.textEncoder.encode(data) : new Uint8Array(data);
1364
+ this.kcp.send(bytes);
1365
+ this.kcp.update(this.now());
1366
+ }
1367
+ close() {
1368
+ this.manualClose = true;
1369
+ this.reconnector.cancel();
1370
+ this.stopUpdateLoop();
1371
+ if (this.transport && (this._state === "open" || this._state === "connecting")) {
1372
+ this._state = "closing";
1373
+ this.transport.close();
1374
+ } else {
1375
+ this.setClosed();
1376
+ }
1377
+ }
1378
+ onOpen(cb) {
1379
+ this.openListeners.add(cb);
1380
+ return () => this.openListeners.delete(cb);
1381
+ }
1382
+ onMessage(cb) {
1383
+ this.messageListeners.add(cb);
1384
+ return () => this.messageListeners.delete(cb);
1385
+ }
1386
+ onError(cb) {
1387
+ this.errorListeners.add(cb);
1388
+ return () => this.errorListeners.delete(cb);
1389
+ }
1390
+ onClose(cb) {
1391
+ this.closeListeners.add(cb);
1392
+ return () => this.closeListeners.delete(cb);
1393
+ }
1394
+ /* --------------------------- 内部实现 --------------------------- */
1395
+ /** 打开底层传输并初始化 KCP;Promise 在传输 open 时 resolve */
1396
+ openTransport() {
1397
+ this.teardownTransport();
1398
+ this._state = "connecting";
1399
+ return new Promise((resolve, reject) => {
1400
+ let transport;
1401
+ try {
1402
+ transport = this.transportFactory(this.url);
1403
+ } catch (e) {
1404
+ this._state = "closed";
1405
+ reject(e instanceof Error ? e : new Error(String(e)));
1406
+ return;
1407
+ }
1408
+ this.transport = transport;
1409
+ let settled = false;
1410
+ this.transportUnsubs.push(
1411
+ transport.onOpen(() => {
1412
+ settled = true;
1413
+ this.setupKcp();
1414
+ this.startUpdateLoop();
1415
+ this._state = "open";
1416
+ this.reconnector.reset();
1417
+ if (this.pendingConnect) {
1418
+ this.pendingConnect.resolve();
1419
+ this.pendingConnect = null;
1420
+ }
1421
+ this.emitOpen();
1422
+ resolve();
1423
+ })
1424
+ );
1425
+ this.transportUnsubs.push(
1426
+ transport.onMessage((ab) => {
1427
+ if (!this.kcp) return;
1428
+ this.kcp.input(new Uint8Array(ab));
1429
+ this.pump();
1430
+ })
1431
+ );
1432
+ this.transportUnsubs.push(
1433
+ transport.onError((err2) => {
1434
+ this.emitError(err2);
1435
+ if (!settled) {
1436
+ settled = true;
1437
+ reject(err2);
1438
+ }
1439
+ })
1440
+ );
1441
+ this.transportUnsubs.push(
1442
+ transport.onClose(() => this.handleClose())
1443
+ );
1444
+ });
1445
+ }
1446
+ /** 初始化 KCP 控制块并接线 output、应用配置 */
1447
+ setupKcp() {
1448
+ const kcp = IKCPCB.create(this.conv, this);
1449
+ kcp.setoutput((data) => {
1450
+ if (!this.transport) return;
1451
+ const copy = data.slice();
1452
+ this.transport.send(copy.buffer);
1453
+ });
1454
+ kcp.nodelay(this.cfgNodelay, this.intervalMs, this.cfgResend, this.cfgNc);
1455
+ if (this.cfgSndWnd !== void 0 || this.cfgRcvWnd !== void 0) {
1456
+ kcp.wndsize(this.cfgSndWnd ?? 0, this.cfgRcvWnd ?? 0);
1457
+ }
1458
+ this.kcp = kcp;
1459
+ }
1460
+ /** 周期定时器:驱动 kcp.update 并把就绪消息吐给业务 */
1461
+ startUpdateLoop() {
1462
+ this.stopUpdateLoop();
1463
+ this.updateTimer = setInterval(() => this.pump(), this.intervalMs);
1464
+ }
1465
+ stopUpdateLoop() {
1466
+ if (this.updateTimer !== null) {
1467
+ clearInterval(this.updateTimer);
1468
+ this.updateTimer = null;
1469
+ }
1470
+ }
1471
+ /** 驱动 KCP 一拍:update(now) + 循环 recv() 把所有就绪消息发给业务 */
1472
+ pump() {
1473
+ if (!this.kcp) return;
1474
+ this.kcp.update(this.now());
1475
+ let msg;
1476
+ while ((msg = this.kcp.recv()) !== null) {
1477
+ const out = msg.slice().buffer;
1478
+ this.emitMessage(out);
1479
+ }
1480
+ }
1481
+ handleClose() {
1482
+ this.stopUpdateLoop();
1483
+ this.teardownTransport();
1484
+ if (this.kcp) {
1485
+ this.kcp.release();
1486
+ this.kcp = null;
1487
+ }
1488
+ if (this.manualClose) {
1489
+ this.setClosed();
1490
+ return;
1491
+ }
1492
+ this._state = "closed";
1493
+ this.emitClose();
1494
+ this.reconnector.schedule();
1495
+ }
1496
+ setClosed() {
1497
+ if (this.kcp) {
1498
+ this.kcp.release();
1499
+ this.kcp = null;
1500
+ }
1501
+ if (this._state === "closed") return;
1502
+ this._state = "closed";
1503
+ this.emitClose();
1504
+ }
1505
+ teardownTransport() {
1506
+ for (const unsub of this.transportUnsubs) {
1507
+ try {
1508
+ unsub();
1509
+ } catch {
1510
+ }
1511
+ }
1512
+ this.transportUnsubs = [];
1513
+ this.transport = null;
1514
+ }
1515
+ /** 当前毫秒时间戳 */
1516
+ now() {
1517
+ return Date.now();
1518
+ }
1519
+ /**
1520
+ * 默认传输实现:基于 PlatformContext 的 IPlatformSocket(WebSocket / wx.connectSocket)。
1521
+ * binaryType=arraybuffer,把它当作 KCP 的"数据报"通道。
1522
+ *
1523
+ * Capacitor 原生 UDP 替换:实现一个同样满足 IKcpTransport 的类(send 走原生 UDP socket,
1524
+ * onMessage 把收到的 datagram 转 ArrayBuffer 回调),通过构造参数 transportFactory 注入。
1525
+ */
1526
+ createDefaultTransport(url) {
1527
+ const options = { url, binaryType: "arraybuffer" };
1528
+ const socket = PlatformContext.current.net.createSocket(options);
1529
+ return {
1530
+ send: (data) => socket.send(data),
1531
+ close: () => socket.close(),
1532
+ onOpen: (cb) => socket.onOpen(cb),
1533
+ onMessage: (cb) => socket.onMessage((data) => {
1534
+ if (typeof data === "string") {
1535
+ this.emitError(
1536
+ new Error("[KcpClient] KCP \u901A\u9053\u6536\u5230\u975E\u4E8C\u8FDB\u5236(string)\u5E27,\u5DF2\u4E22\u5F03;KCP \u901A\u9053\u5E94\u4E3A ArrayBuffer")
1537
+ );
1538
+ return;
1539
+ }
1540
+ cb(data);
1541
+ }),
1542
+ onError: (cb) => socket.onError(cb),
1543
+ onClose: (cb) => socket.onClose(() => cb())
1544
+ };
1545
+ }
1546
+ emitOpen() {
1547
+ for (const cb of this.openListeners) cb();
1548
+ }
1549
+ emitMessage(data) {
1550
+ for (const cb of this.messageListeners) cb(data);
1551
+ }
1552
+ emitError(err2) {
1553
+ for (const cb of this.errorListeners) cb(err2);
1554
+ }
1555
+ emitClose() {
1556
+ for (const cb of this.closeListeners) cb();
1557
+ }
1558
+ };
1559
+
1560
+ // src/net/NetManager.ts
1561
+ var NetManager = class {
1562
+ constructor(opts = {}) {
1563
+ __publicField(this, "apiBaseURL");
1564
+ /** defaultHttp 懒加载单例 */
1565
+ __publicField(this, "_defaultHttp", null);
1566
+ this.apiBaseURL = opts.apiBaseURL ?? "";
1567
+ }
1568
+ /** 创建一个独立的 HTTP 客户端;不传 config 时默认带上全局 apiBaseURL */
1569
+ http(config) {
1570
+ if (config) {
1571
+ return new HttpClient({
1572
+ baseURL: this.apiBaseURL,
1573
+ ...config
1574
+ });
1575
+ }
1576
+ return new HttpClient({ baseURL: this.apiBaseURL });
1577
+ }
1578
+ /** 创建 WebSocket 长连接客户端 */
1579
+ websocket(config) {
1580
+ return new WebSocketClient(config);
1581
+ }
1582
+ /** 创建 KCP 长连接客户端 */
1583
+ kcp(config) {
1584
+ return new KcpClient(config);
1585
+ }
1586
+ /** 框架级默认 HTTP 单例(读取构造时的 apiBaseURL) */
1587
+ get defaultHttp() {
1588
+ if (!this._defaultHttp) {
1589
+ this._defaultHttp = new HttpClient({ baseURL: this.apiBaseURL });
1590
+ }
1591
+ return this._defaultHttp;
1592
+ }
1593
+ };
1594
+
1595
+ // src/services/EventBus.ts
1596
+ var EventBus = class {
1597
+ constructor() {
1598
+ /** 事件名 -> 订阅列表 */
1599
+ __publicField(this, "map", /* @__PURE__ */ new Map());
1600
+ }
1601
+ on(event, handler) {
1602
+ const h = handler;
1603
+ this.addSubscription(event, { original: h, actual: h });
1604
+ return () => this.off(event, h);
1605
+ }
1606
+ once(event, handler) {
1607
+ const original = handler;
1608
+ const wrapper = (payload) => {
1609
+ this.removeSubscription(event, original);
1610
+ original(payload);
1611
+ };
1612
+ this.addSubscription(event, { original, actual: wrapper });
1613
+ return () => this.off(event, original);
1614
+ }
1615
+ off(event, handler) {
1616
+ this.removeSubscription(event, handler);
1617
+ }
1618
+ emit(event, payload) {
1619
+ const subs = this.map.get(event);
1620
+ if (!subs || subs.length === 0) return;
1621
+ const snapshot = subs.slice();
1622
+ for (const sub of snapshot) {
1623
+ sub.actual(payload);
1624
+ }
1625
+ }
1626
+ clear(event) {
1627
+ if (event === void 0) {
1628
+ this.map.clear();
1629
+ } else {
1630
+ this.map.delete(event);
1631
+ }
1632
+ }
1633
+ /** 追加一条订阅 */
1634
+ addSubscription(event, sub) {
1635
+ const list = this.map.get(event);
1636
+ if (list) {
1637
+ list.push(sub);
1638
+ } else {
1639
+ this.map.set(event, [sub]);
1640
+ }
1641
+ }
1642
+ /** 按原始 handler 移除订阅(只移除第一处匹配,与 on 多次注册语义对应) */
1643
+ removeSubscription(event, original) {
1644
+ const list = this.map.get(event);
1645
+ if (!list) return;
1646
+ const idx = list.findIndex((s) => s.original === original);
1647
+ if (idx >= 0) {
1648
+ list.splice(idx, 1);
1649
+ if (list.length === 0) this.map.delete(event);
1650
+ }
1651
+ }
1652
+ };
1653
+
1654
+ // src/types.ts
1655
+ function ok(value) {
1656
+ return { ok: true, value };
1657
+ }
1658
+ function err(error) {
1659
+ return { ok: false, error: typeof error === "string" ? new Error(error) : error };
1660
+ }
1661
+ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
1662
+ LogLevel2[LogLevel2["Debug"] = 0] = "Debug";
1663
+ LogLevel2[LogLevel2["Info"] = 1] = "Info";
1664
+ LogLevel2[LogLevel2["Warn"] = 2] = "Warn";
1665
+ LogLevel2[LogLevel2["Error"] = 3] = "Error";
1666
+ LogLevel2[LogLevel2["Silent"] = 4] = "Silent";
1667
+ return LogLevel2;
1668
+ })(LogLevel || {});
1669
+
1670
+ // src/services/Logger.ts
1671
+ var Logger = class _Logger {
1672
+ /**
1673
+ * @param production 是否生产环境(true 时起步级别 Warn)
1674
+ * @param ref 内部共享级别容器(子 logger 复用根的引用,外部不传)
1675
+ * @param tags 标签链(子 logger 内部传递,外部不传)
1676
+ */
1677
+ constructor(production = false, ref, tags = []) {
1678
+ __publicField(this, "ref");
1679
+ /** 标签前缀链,如 ['Net', 'Http'] => 输出 "[Net][Http]" */
1680
+ __publicField(this, "tags");
1681
+ this.ref = ref ?? { level: production ? 2 /* Warn */ : 0 /* Debug */ };
1682
+ this.tags = tags;
1683
+ }
1684
+ get level() {
1685
+ return this.ref.level;
1686
+ }
1687
+ set level(v) {
1688
+ this.ref.level = v;
1689
+ }
1690
+ debug(...args) {
1691
+ this.output(0 /* Debug */, args);
1692
+ }
1693
+ info(...args) {
1694
+ this.output(1 /* Info */, args);
1695
+ }
1696
+ warn(...args) {
1697
+ this.output(2 /* Warn */, args);
1698
+ }
1699
+ error(...args) {
1700
+ this.output(3 /* Error */, args);
1701
+ }
1702
+ tag(label) {
1703
+ return new _Logger(false, this.ref, [...this.tags, label]);
1704
+ }
1705
+ /** 级别过滤 + 前缀拼装 + 分流到 console 对应方法 */
1706
+ output(level, args) {
1707
+ if (level < this.ref.level) return;
1708
+ const prefix = this.tags.length ? this.tags.map((t) => `[${t}]`).join("") : "";
1709
+ const payload = prefix ? [prefix, ...args] : args;
1710
+ switch (level) {
1711
+ case 0 /* Debug */:
1712
+ console.debug(...payload);
1713
+ break;
1714
+ case 1 /* Info */:
1715
+ console.info(...payload);
1716
+ break;
1717
+ case 2 /* Warn */:
1718
+ console.warn(...payload);
1719
+ break;
1720
+ case 3 /* Error */:
1721
+ console.error(...payload);
1722
+ break;
1723
+ }
1724
+ }
1725
+ };
1726
+
1727
+ // src/services/ConfigService.ts
1728
+ var ConfigService = class {
1729
+ constructor(config) {
1730
+ __publicField(this, "_value");
1731
+ this._value = deepFreeze({
1732
+ ...config,
1733
+ extra: config.extra ? { ...config.extra } : void 0
1734
+ });
1735
+ }
1736
+ get value() {
1737
+ return this._value;
1738
+ }
1739
+ /**
1740
+ * 取配置值:
1741
+ * 1) 命中顶层 AppConfig 字段(apiBaseURL/designWidth/...)直接返回
1742
+ * 2) 否则尝试 extra[key]
1743
+ * 3) 都没有则返回 fallback
1744
+ */
1745
+ get(key, fallback) {
1746
+ const top = this._value[key];
1747
+ if (top !== void 0) return top;
1748
+ const extra = this._value.extra;
1749
+ if (extra && key in extra) {
1750
+ return extra[key];
1751
+ }
1752
+ return fallback;
1753
+ }
1754
+ };
1755
+ function deepFreeze(obj) {
1756
+ if (obj && typeof obj === "object") {
1757
+ for (const v of Object.values(obj)) {
1758
+ if (v && typeof v === "object" && !Object.isFrozen(v)) {
1759
+ deepFreeze(v);
1760
+ }
1761
+ }
1762
+ Object.freeze(obj);
1763
+ }
1764
+ return obj;
1765
+ }
1766
+
1767
+ // src/services/StorageManager.ts
1768
+ var StorageManager = class {
1769
+ constructor(prefix = "app:") {
1770
+ /** 键前缀,隔离命名空间 */
1771
+ __publicField(this, "prefix");
1772
+ this.prefix = prefix;
1773
+ }
1774
+ get(key, fallback) {
1775
+ try {
1776
+ const raw = PlatformContext.current.storage.getItem(this.k(key));
1777
+ if (raw === null || raw === void 0) return fallback;
1778
+ return JSON.parse(raw);
1779
+ } catch {
1780
+ return fallback;
1781
+ }
1782
+ }
1783
+ set(key, value) {
1784
+ try {
1785
+ const raw = JSON.stringify(value);
1786
+ PlatformContext.current.storage.setItem(this.k(key), raw);
1787
+ } catch {
1788
+ }
1789
+ }
1790
+ remove(key) {
1791
+ try {
1792
+ PlatformContext.current.storage.removeItem(this.k(key));
1793
+ } catch {
1794
+ }
1795
+ }
1796
+ clear() {
1797
+ try {
1798
+ PlatformContext.current.storage.clear();
1799
+ } catch {
1800
+ }
1801
+ }
1802
+ /** 拼接命名空间前缀 */
1803
+ k(key) {
1804
+ return this.prefix + key;
1805
+ }
1806
+ };
1807
+
1808
+ // src/services/AudioManager.ts
1809
+ var AudioManager = class {
1810
+ constructor() {
1811
+ /** bootstrap 注入的 Phaser.Game,延迟绑定 */
1812
+ __publicField(this, "game", null);
1813
+ /** 当前播放的背景音乐实例 */
1814
+ __publicField(this, "music", null);
1815
+ __publicField(this, "musicVolume", 1);
1816
+ __publicField(this, "sfxVolume", 1);
1817
+ __publicField(this, "muted", false);
1818
+ }
1819
+ /** 由 bootstrap 在 Phaser.Game 创建后调用,完成绑定 */
1820
+ bindGame(game) {
1821
+ this.game = game;
1822
+ }
1823
+ playMusic(key, opts) {
1824
+ const sound = this.game?.sound;
1825
+ if (!sound) return;
1826
+ this.stopMusic();
1827
+ const volume = (opts?.volume ?? 1) * this.musicVolume;
1828
+ const inst = sound.add(key, {
1829
+ loop: opts?.loop ?? true,
1830
+ volume,
1831
+ mute: this.muted
1832
+ });
1833
+ inst.play();
1834
+ this.music = inst;
1835
+ }
1836
+ stopMusic() {
1837
+ if (this.music) {
1838
+ this.music.stop();
1839
+ this.music.destroy();
1840
+ this.music = null;
1841
+ }
1842
+ }
1843
+ playSfx(key, opts) {
1844
+ const sound = this.game?.sound;
1845
+ if (!sound) return;
1846
+ const volume = (opts?.volume ?? 1) * this.sfxVolume;
1847
+ sound.play(key, { volume, mute: this.muted });
1848
+ }
1849
+ setMusicVolume(v) {
1850
+ this.musicVolume = clamp01(v);
1851
+ const m = this.music;
1852
+ m?.setVolume?.(this.musicVolume);
1853
+ }
1854
+ setSfxVolume(v) {
1855
+ this.sfxVolume = clamp01(v);
1856
+ }
1857
+ muteAll(muted) {
1858
+ this.muted = muted;
1859
+ const sound = this.game?.sound;
1860
+ if (sound) {
1861
+ sound.mute = muted;
1862
+ }
1863
+ }
1864
+ };
1865
+ function clamp01(v) {
1866
+ if (Number.isNaN(v)) return 0;
1867
+ return Math.min(1, Math.max(0, v));
1868
+ }
1869
+
1870
+ // src/services/AssetLoader.ts
1871
+ var AssetLoader = class {
1872
+ constructor(baseUrl = "") {
1873
+ /** 资源基址(相对路径会拼到它后面);默认空表示相对当前页面 */
1874
+ __publicField(this, "baseUrl");
1875
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
1876
+ }
1877
+ resolveUrl(path) {
1878
+ if (/^https?:\/\//i.test(path) || /^data:/i.test(path)) return path;
1879
+ if (!this.baseUrl) return path;
1880
+ const p = path.replace(/^\/+/, "");
1881
+ return p ? `${this.baseUrl}/${p}` : this.baseUrl;
1882
+ }
1883
+ async loadText(path) {
1884
+ const resp = await this.request(path, "text");
1885
+ return typeof resp.data === "string" ? resp.data : String(resp.data ?? "");
1886
+ }
1887
+ async loadJson(path) {
1888
+ const resp = await this.request(path, "json");
1889
+ if (typeof resp.data === "string") {
1890
+ return JSON.parse(resp.data);
1891
+ }
1892
+ return resp.data;
1893
+ }
1894
+ async loadArrayBuffer(path) {
1895
+ const resp = await this.request(path, "arraybuffer");
1896
+ const data = resp.data;
1897
+ if (data instanceof ArrayBuffer) return data;
1898
+ if (ArrayBuffer.isView(data)) {
1899
+ const view = data;
1900
+ return view.buffer.slice(
1901
+ view.byteOffset,
1902
+ view.byteOffset + view.byteLength
1903
+ );
1904
+ }
1905
+ throw new Error(`[AssetLoader] \u671F\u671B ArrayBuffer,\u5B9E\u9645\u6536\u5230: ${typeof data}`);
1906
+ }
1907
+ /** 统一发起平台层请求,失败时抛出携带 path 的清晰错误 */
1908
+ async request(path, responseType) {
1909
+ const url = this.resolveUrl(path);
1910
+ const resp = await PlatformContext.current.net.request({
1911
+ url,
1912
+ method: "GET",
1913
+ responseType
1914
+ });
1915
+ if (!resp.ok) {
1916
+ throw new Error(`[AssetLoader] \u52A0\u8F7D\u5931\u8D25 ${resp.status}: ${url}`);
1917
+ }
1918
+ return resp;
1919
+ }
1920
+ };
1921
+
1922
+ // src/services/AdsManager.ts
1923
+ var AdsManager = class {
1924
+ /**
1925
+ * @param opts 默认广告位配置(由 bootstrap 从 config.ads 传入,可缺省)
1926
+ */
1927
+ constructor(opts = {}) {
1928
+ /** 激励视频默认广告位 id(调用未传 id 时回退到它) */
1929
+ __publicField(this, "defaultRewardedUnitId");
1930
+ /** 插屏默认广告位 id(调用未传 id 时回退到它) */
1931
+ __publicField(this, "defaultInterstitialUnitId");
1932
+ this.defaultRewardedUnitId = opts.rewardedUnitId;
1933
+ this.defaultInterstitialUnitId = opts.interstitialUnitId;
1934
+ }
1935
+ /** 取当前平台广告能力,可能为 undefined */
1936
+ get impl() {
1937
+ return PlatformContext.current.ads;
1938
+ }
1939
+ get available() {
1940
+ return this.impl !== void 0;
1941
+ }
1942
+ async preloadRewarded(adUnitId) {
1943
+ const ads = this.impl;
1944
+ if (!ads) return;
1945
+ try {
1946
+ await ads.preloadRewarded(adUnitId ?? this.defaultRewardedUnitId);
1947
+ } catch {
1948
+ }
1949
+ }
1950
+ async showRewarded(adUnitId) {
1951
+ const ads = this.impl;
1952
+ if (!ads) return false;
1953
+ try {
1954
+ const result = await ads.showRewarded(adUnitId ?? this.defaultRewardedUnitId);
1955
+ return result === "completed";
1956
+ } catch {
1957
+ return false;
1958
+ }
1959
+ }
1960
+ async showInterstitial(adUnitId) {
1961
+ const ads = this.impl;
1962
+ if (!ads) return "failed";
1963
+ try {
1964
+ return await ads.showInterstitial(adUnitId ?? this.defaultInterstitialUnitId);
1965
+ } catch {
1966
+ return "failed";
1967
+ }
1968
+ }
1969
+ };
1970
+
1971
+ // src/services/PaymentManager.ts
1972
+ var PaymentManager = class {
1973
+ get impl() {
1974
+ return PlatformContext.current.payment;
1975
+ }
1976
+ get available() {
1977
+ return this.impl !== void 0;
1978
+ }
1979
+ async purchase(productId, extra) {
1980
+ const payment = this.impl;
1981
+ if (!payment) {
1982
+ throw new PlatformUnsupportedError(
1983
+ PlatformContext.current.info.name,
1984
+ "payment.purchase"
1985
+ );
1986
+ }
1987
+ return payment.purchase(productId, extra);
1988
+ }
1989
+ async restore() {
1990
+ const payment = this.impl;
1991
+ if (!payment || !payment.restore) return [];
1992
+ try {
1993
+ return await payment.restore();
1994
+ } catch {
1995
+ return [];
1996
+ }
1997
+ }
1998
+ };
1999
+
2000
+ // src/services/AnalyticsManager.ts
2001
+ var AnalyticsManager = class {
2002
+ constructor() {
2003
+ __publicField(this, "userId");
2004
+ __publicField(this, "backendClient");
2005
+ __publicField(this, "reportPath", "/analytics/track");
2006
+ __publicField(this, "backendEnabled", false);
2007
+ __publicField(this, "onError");
2008
+ }
2009
+ /**
2010
+ * 由 bootstrap 注入自有后端上报能力(在 NetManager 就绪后调用)。
2011
+ *
2012
+ * 职责拆分(D4):
2013
+ * - configureBackend 负责"装配通道"——注入 HTTP client、reportPath、错误回调;
2014
+ * 是否真正启用由 opts.enabled 决定(bootstrap 通常先以 enabled:false 装配)。
2015
+ * - enableBackend(IAnalyticsManager 正式接口)负责"启用开关"——在通道已装配后
2016
+ * 把 backendEnabled 置真,供 bootstrap 据 config.analytics.backendEnabled 或业务运行时调用。
2017
+ */
2018
+ configureBackend(opts) {
2019
+ this.backendClient = opts.client;
2020
+ if (opts.reportPath) this.reportPath = opts.reportPath;
2021
+ this.backendEnabled = opts.enabled ?? Boolean(opts.client);
2022
+ this.onError = opts.onError;
2023
+ }
2024
+ /**
2025
+ * 启用自有后端上报(IAnalyticsManager 正式接口)。
2026
+ * 需先经 configureBackend 注入 client;未注入 client 时仅置开关、无实际上报(reportToBackend 会短路)。
2027
+ * @param opts.reportPath 可选,覆盖上报路径
2028
+ */
2029
+ enableBackend(opts) {
2030
+ if (opts?.reportPath) this.reportPath = opts.reportPath;
2031
+ this.backendEnabled = true;
2032
+ }
2033
+ get platformImpl() {
2034
+ return PlatformContext.current.analytics;
2035
+ }
2036
+ track(event, params) {
2037
+ const enriched = {
2038
+ ...params ?? {},
2039
+ ...this.userId ? { userId: this.userId } : {},
2040
+ ts: Date.now()
2041
+ };
2042
+ try {
2043
+ this.platformImpl?.reportEvent(event, enriched);
2044
+ } catch {
2045
+ }
2046
+ this.reportToBackend(event, enriched);
2047
+ }
2048
+ setUserId(id) {
2049
+ this.userId = id;
2050
+ try {
2051
+ this.platformImpl?.reportEvent("set_user_id", { userId: id });
2052
+ } catch {
2053
+ }
2054
+ }
2055
+ /** 异步上报到自有后端,失败静默(可经 onError 观测) */
2056
+ reportToBackend(event, params) {
2057
+ if (!this.backendEnabled || !this.backendClient) return;
2058
+ this.backendClient.post(this.reportPath, { event, params }).catch((e) => {
2059
+ const err2 = e instanceof Error ? e : new Error(String(e));
2060
+ this.onError?.(err2);
2061
+ });
2062
+ }
2063
+ };
2064
+
2065
+ // src/services/AccountService.ts
2066
+ var AccountService = class {
2067
+ get impl() {
2068
+ return PlatformContext.current.auth;
2069
+ }
2070
+ async login() {
2071
+ const auth = this.impl;
2072
+ if (!auth) {
2073
+ throw new PlatformUnsupportedError(
2074
+ PlatformContext.current.info.name,
2075
+ "auth.login"
2076
+ );
2077
+ }
2078
+ return auth.login();
2079
+ }
2080
+ /**
2081
+ * 获取用户公开信息(昵称 / 头像)。D5。
2082
+ *
2083
+ * 委托平台 auth.getProfile();无 auth 能力时返回 null(**不抛**)——
2084
+ * getProfile 是可选信息,平台不支持或用户拒绝授权应静默降级,
2085
+ * 业务据此展示默认头像/昵称,而非阻断流程(与 login 不可静默不同)。
2086
+ */
2087
+ async getProfile() {
2088
+ const auth = this.impl;
2089
+ if (!auth) return null;
2090
+ return auth.getProfile();
2091
+ }
2092
+ };
2093
+
2094
+ // src/services/SceneManager.ts
2095
+ var SceneManager = class {
2096
+ constructor() {
2097
+ /** bootstrap 注入的 Phaser.Game,延迟绑定 */
2098
+ __publicField(this, "game", null);
2099
+ }
2100
+ /** 由 bootstrap 在 Phaser.Game 创建后调用 */
2101
+ bindGame(game) {
2102
+ this.game = game;
2103
+ }
2104
+ goto(key, data) {
2105
+ const manager = this.game?.scene;
2106
+ if (!manager) {
2107
+ console.warn("[SceneManager] Phaser.Game \u5C1A\u672A\u5C31\u7EEA,goto \u88AB\u5FFD\u7565:", key);
2108
+ return;
2109
+ }
2110
+ for (const scene of manager.getScenes(true)) {
2111
+ const sceneKey = scene.scene.key;
2112
+ if (sceneKey !== key) {
2113
+ manager.stop(sceneKey);
2114
+ }
2115
+ }
2116
+ manager.start(key, asSceneData(data));
2117
+ }
2118
+ push(key, data) {
2119
+ const manager = this.game?.scene;
2120
+ if (!manager) {
2121
+ console.warn("[SceneManager] Phaser.Game \u5C1A\u672A\u5C31\u7EEA,push \u88AB\u5FFD\u7565:", key);
2122
+ return;
2123
+ }
2124
+ if (manager.isActive(key)) {
2125
+ manager.bringToTop(key);
2126
+ return;
2127
+ }
2128
+ manager.run(key, asSceneData(data));
2129
+ manager.bringToTop(key);
2130
+ }
2131
+ pop(key) {
2132
+ const manager = this.game?.scene;
2133
+ if (!manager) {
2134
+ console.warn("[SceneManager] Phaser.Game \u5C1A\u672A\u5C31\u7EEA,pop \u88AB\u5FFD\u7565:", key);
2135
+ return;
2136
+ }
2137
+ if (manager.isActive(key) || manager.isPaused(key) || manager.isSleeping(key)) {
2138
+ manager.stop(key);
2139
+ }
2140
+ }
2141
+ };
2142
+ function asSceneData(data) {
2143
+ if (data === void 0 || data === null) return void 0;
2144
+ if (typeof data === "object") return data;
2145
+ return { value: data };
2146
+ }
2147
+
2148
+ // src/i18n/I18n.ts
2149
+ var I18n = class {
2150
+ constructor(opts = {}) {
2151
+ /** 当前语言(已归一) */
2152
+ __publicField(this, "_locale");
2153
+ /** 兜底语言(已归一) */
2154
+ __publicField(this, "fallbackLocale");
2155
+ /** 语言包:locale → (key → 文案) */
2156
+ __publicField(this, "bundles", /* @__PURE__ */ new Map());
2157
+ this.fallbackLocale = normalizeLocale(opts.fallbackLocale ?? "en-US");
2158
+ this._locale = normalizeLocale(opts.locale ?? this.fallbackLocale);
2159
+ }
2160
+ /** 当前语言(归一后的 xx-XX 形态) */
2161
+ get locale() {
2162
+ return this._locale;
2163
+ }
2164
+ /** 切换当前语言(入参自动归一) */
2165
+ setLocale(locale) {
2166
+ this._locale = normalizeLocale(locale);
2167
+ }
2168
+ /**
2169
+ * 合并注册一段语言包。多次对同一 locale 调用会浅合并(后注册覆盖同名 key),
2170
+ * 便于按模块分包加载(如先注册通用包,再注册某场景专属包)。
2171
+ * locale 入参自动归一。
2172
+ */
2173
+ addBundle(locale, dict) {
2174
+ const norm = normalizeLocale(locale);
2175
+ const existing = this.bundles.get(norm);
2176
+ if (existing) {
2177
+ Object.assign(existing, dict);
2178
+ } else {
2179
+ this.bundles.set(norm, { ...dict });
2180
+ }
2181
+ }
2182
+ /**
2183
+ * 取文案。
2184
+ * 查找顺序:当前 locale → fallback locale → 返回 key 本身(并 warn)。
2185
+ * 命中后用 params 做 {name} 占位替换。
2186
+ *
2187
+ * @param key 文案键
2188
+ * @param params 占位参数,如 t('hello', { name: 'Tom' }) 替换 "{name}"
2189
+ */
2190
+ t(key, params) {
2191
+ const template = this.lookup(key);
2192
+ if (template === void 0) {
2193
+ App.log.warn(`[I18n] \u7F3A\u5931\u6587\u6848 key='${key}'(locale='${this._locale}')`);
2194
+ return key;
2195
+ }
2196
+ return params ? interpolate(template, params) : template;
2197
+ }
2198
+ /** 当前 locale 或 fallback locale 中是否存在该 key */
2199
+ has(key) {
2200
+ return this.lookup(key) !== void 0;
2201
+ }
2202
+ /* ------------------------------ 内部 ------------------------------ */
2203
+ /**
2204
+ * 按"当前 locale → fallback locale"顺序查找原始模板,均无则 undefined。
2205
+ * 不在此 warn(由 t 决定是否 warn,has 不应产生噪声日志)。
2206
+ */
2207
+ lookup(key) {
2208
+ const cur = this.bundles.get(this._locale);
2209
+ if (cur && key in cur) return cur[key];
2210
+ if (this.fallbackLocale !== this._locale) {
2211
+ const fb = this.bundles.get(this.fallbackLocale);
2212
+ if (fb && key in fb) return fb[key];
2213
+ }
2214
+ return void 0;
2215
+ }
2216
+ };
2217
+ function interpolate(template, params) {
2218
+ return template.replace(/\{(\w+)\}/g, (match, name) => {
2219
+ const v = params[name];
2220
+ return v === void 0 ? match : String(v);
2221
+ });
2222
+ }
2223
+ function normalizeLocale(raw) {
2224
+ if (!raw || typeof raw !== "string") return "en-US";
2225
+ const parts2 = raw.trim().replace(/_/g, "-").split("-").filter(Boolean);
2226
+ if (parts2.length === 0) return "en-US";
2227
+ const lang = parts2[0].toLowerCase();
2228
+ const rest = parts2.slice(1);
2229
+ let script;
2230
+ let region;
2231
+ for (const seg of rest) {
2232
+ if (/^[a-z]{4}$/i.test(seg)) {
2233
+ script = seg.toLowerCase();
2234
+ } else if (/^[a-z]{2}$/i.test(seg) || /^\d{3}$/.test(seg)) {
2235
+ region = seg.toUpperCase();
2236
+ }
2237
+ }
2238
+ if (!region && script) {
2239
+ if (lang === "zh") {
2240
+ region = script === "hant" ? "TW" : "CN";
2241
+ }
2242
+ }
2243
+ return region ? `${lang}-${region}` : lang;
2244
+ }
2245
+
2246
+ // src/save/SaveManager.ts
2247
+ function deepClone(value) {
2248
+ try {
2249
+ return JSON.parse(JSON.stringify(value));
2250
+ } catch {
2251
+ return value;
2252
+ }
2253
+ }
2254
+ function checksum(text) {
2255
+ let hash = 2166136261;
2256
+ for (let i = 0; i < text.length; i++) {
2257
+ hash ^= text.charCodeAt(i);
2258
+ hash = Math.imul(hash, 16777619);
2259
+ }
2260
+ return (hash >>> 0).toString(16).padStart(8, "0");
2261
+ }
2262
+ var SaveManager = class {
2263
+ constructor() {
2264
+ /** 已注册的存档槽,按 key 去重 */
2265
+ __publicField(this, "slots", /* @__PURE__ */ new Map());
2266
+ }
2267
+ /**
2268
+ * 注册一个存档槽。返回的 SaveSlot 即业务读写存档的唯一入口。
2269
+ * @template T 存档数据结构
2270
+ */
2271
+ register(opts) {
2272
+ const existing = this.slots.get(opts.key);
2273
+ if (existing) {
2274
+ App.log.warn(
2275
+ `[SaveManager] \u5B58\u6863\u69FD '${opts.key}' \u5DF2\u6CE8\u518C,\u5FFD\u7565\u91CD\u590D\u6CE8\u518C(\u6CBF\u7528\u9996\u6B21\u914D\u7F6E)`
2276
+ );
2277
+ return existing;
2278
+ }
2279
+ const slot = new SaveSlotImpl(opts);
2280
+ this.slots.set(opts.key, slot);
2281
+ return slot;
2282
+ }
2283
+ };
2284
+ var SaveSlotImpl = class {
2285
+ constructor(opts) {
2286
+ /** 存储键 */
2287
+ __publicField(this, "key");
2288
+ /** 当前版本 */
2289
+ __publicField(this, "_version");
2290
+ /** 默认值(只读源,对外永远给深拷贝) */
2291
+ __publicField(this, "defaults");
2292
+ /** 迁移函数(可选) */
2293
+ __publicField(this, "migrate");
2294
+ this.key = opts.key;
2295
+ this._version = opts.version;
2296
+ this.defaults = opts.defaults;
2297
+ this.migrate = opts.migrate;
2298
+ }
2299
+ get version() {
2300
+ return this._version;
2301
+ }
2302
+ load() {
2303
+ const record = App.storage.get(this.key);
2304
+ if (!this.isValidRecordShape(record)) {
2305
+ return deepClone(this.defaults);
2306
+ }
2307
+ if (!this.verify(record)) {
2308
+ App.log.warn(
2309
+ `[SaveManager] \u5B58\u6863 '${this.key}' \u6821\u9A8C\u5931\u8D25(\u53EF\u80FD\u88AB\u7BE1\u6539\u6216\u635F\u574F),\u56DE\u9000\u9ED8\u8BA4\u503C`
2310
+ );
2311
+ return deepClone(this.defaults);
2312
+ }
2313
+ if (record.v === this._version) {
2314
+ return record.data;
2315
+ }
2316
+ if (record.v > this._version) {
2317
+ App.log.warn(
2318
+ `[SaveManager] \u5B58\u6863 '${this.key}' \u7248\u672C(${record.v})\u9AD8\u4E8E\u5F53\u524D(${this._version}),\u7591\u4F3C\u6E38\u620F\u7248\u672C\u56DE\u9000,\u56DE\u9000\u9ED8\u8BA4\u503C`
2319
+ );
2320
+ return deepClone(this.defaults);
2321
+ }
2322
+ return this.runMigration(record.data, record.v);
2323
+ }
2324
+ save(data) {
2325
+ const record = this.buildRecord(data);
2326
+ App.storage.set(this.key, record);
2327
+ }
2328
+ patch(partial) {
2329
+ const current = this.load();
2330
+ const merged = { ...current, ...partial };
2331
+ this.save(merged);
2332
+ }
2333
+ reset() {
2334
+ this.save(deepClone(this.defaults));
2335
+ }
2336
+ /* ------------------------------ 内部 ------------------------------ */
2337
+ /** 把 data 包装成落盘记录(算校验和) */
2338
+ buildRecord(data) {
2339
+ return {
2340
+ v: this._version,
2341
+ data,
2342
+ sum: this.computeSum(data)
2343
+ };
2344
+ }
2345
+ /** 对 data 计算校验和:序列化失败时回退空串(verify 时也会算到一致值) */
2346
+ computeSum(data) {
2347
+ let serialized;
2348
+ try {
2349
+ serialized = JSON.stringify(data) ?? "";
2350
+ } catch {
2351
+ serialized = "";
2352
+ }
2353
+ return checksum(serialized);
2354
+ }
2355
+ /** 校验记录:重算 data 的校验和与存档中 sum 是否一致 */
2356
+ verify(record) {
2357
+ return this.computeSum(record.data) === record.sum;
2358
+ }
2359
+ /** 运行迁移:有 migrate 则升级并写回,无 migrate 则 warn 回退 defaults */
2360
+ runMigration(oldData, oldVersion) {
2361
+ if (!this.migrate) {
2362
+ App.log.warn(
2363
+ `[SaveManager] \u5B58\u6863 '${this.key}' \u7248\u672C\u843D\u540E(${oldVersion} \u2192 ${this._version})\u4F46\u672A\u63D0\u4F9B migrate,\u56DE\u9000\u9ED8\u8BA4\u503C`
2364
+ );
2365
+ const fresh = deepClone(this.defaults);
2366
+ this.save(fresh);
2367
+ return fresh;
2368
+ }
2369
+ try {
2370
+ const migrated = this.migrate(oldData, oldVersion);
2371
+ this.save(migrated);
2372
+ App.log.info(
2373
+ `[SaveManager] \u5B58\u6863 '${this.key}' \u5DF2\u8FC1\u79FB(${oldVersion} \u2192 ${this._version})`
2374
+ );
2375
+ return migrated;
2376
+ } catch (e) {
2377
+ App.log.warn(
2378
+ `[SaveManager] \u5B58\u6863 '${this.key}' \u8FC1\u79FB\u5931\u8D25,\u56DE\u9000\u9ED8\u8BA4\u503C:`,
2379
+ e instanceof Error ? e.message : String(e)
2380
+ );
2381
+ const fresh = deepClone(this.defaults);
2382
+ this.save(fresh);
2383
+ return fresh;
2384
+ }
2385
+ }
2386
+ /** 粗校验记录形状:必须是带数字 v、字符串 sum、含 data 字段的对象 */
2387
+ isValidRecordShape(record) {
2388
+ if (record === null || typeof record !== "object") return false;
2389
+ const r = record;
2390
+ return typeof r.v === "number" && typeof r.sum === "string" && "data" in r;
2391
+ }
2392
+ };
2393
+
2394
+ // src/config/ConfigTable.ts
2395
+ var ConfigTable = class {
2396
+ /**
2397
+ * @param rows 已构造好的行集合
2398
+ * @param primaryKey 主键字段名(默认 'id'),用于建立索引
2399
+ */
2400
+ constructor(rows, primaryKey = "id") {
2401
+ /** 主键(string|number 统一转 string 作为 Map key) -> 行数据 */
2402
+ __publicField(this, "index", /* @__PURE__ */ new Map());
2403
+ for (const row of rows) {
2404
+ const key = row[primaryKey];
2405
+ if (key === void 0 || key === null) {
2406
+ continue;
2407
+ }
2408
+ this.index.set(String(key), row);
2409
+ }
2410
+ }
2411
+ /** 按主键取一行;不存在返回 undefined */
2412
+ get(id) {
2413
+ return this.index.get(String(id));
2414
+ }
2415
+ /** 返回全部行(顺序为插入顺序) */
2416
+ all() {
2417
+ return Array.from(this.index.values());
2418
+ }
2419
+ /** 是否存在某主键 */
2420
+ has(id) {
2421
+ return this.index.has(String(id));
2422
+ }
2423
+ /** 行数 */
2424
+ get size() {
2425
+ return this.index.size;
2426
+ }
2427
+ };
2428
+
2429
+ // src/config/ConfigTableManager.ts
2430
+ var ConfigTableManager = class {
2431
+ constructor() {
2432
+ /** 表名 -> 已建好的配置表(用 unknown 擦除元素类型,get<T> 时再断言) */
2433
+ __publicField(this, "tables", /* @__PURE__ */ new Map());
2434
+ }
2435
+ /**
2436
+ * 按名加载并缓存一张配置表。已缓存则直接返回缓存,不重复请求。
2437
+ *
2438
+ * @param name 表名(后续 get/has/unload 的键)
2439
+ * @param path 资源路径(交给 App.assets.loadJson)
2440
+ * @param primaryKey 主键字段名,默认 'id'
2441
+ */
2442
+ async load(name, path, primaryKey = "id") {
2443
+ const cached = this.tables.get(name);
2444
+ if (cached) return cached;
2445
+ const data = await App.assets.loadJson(path);
2446
+ const rows = this.normalize(data, primaryKey);
2447
+ const table = new ConfigTable(rows, primaryKey);
2448
+ this.tables.set(name, table);
2449
+ return table;
2450
+ }
2451
+ /**
2452
+ * 取已加载的表;未加载抛出清晰错误(提示先 load)。
2453
+ */
2454
+ get(name) {
2455
+ const table = this.tables.get(name);
2456
+ if (!table) {
2457
+ throw new Error(
2458
+ `[ConfigTableManager] \u914D\u7F6E\u8868 "${name}" \u5C1A\u672A\u52A0\u8F7D\u3002\u8BF7\u5148\u8C03\u7528 load("${name}", path) \u540E\u518D get\u3002`
2459
+ );
2460
+ }
2461
+ return table;
2462
+ }
2463
+ /** 是否已加载某表 */
2464
+ has(name) {
2465
+ return this.tables.has(name);
2466
+ }
2467
+ /** 卸载某表缓存(下次 load 会重新请求);返回是否确有该表 */
2468
+ unload(name) {
2469
+ return this.tables.delete(name);
2470
+ }
2471
+ /**
2472
+ * 把数组或字典两种形态归一化成行数组。
2473
+ * - 数组:直接使用。
2474
+ * - 字典:取所有值;若某值缺主键,则用字典 key 回填(便于字典形态省略 id)。
2475
+ */
2476
+ normalize(data, primaryKey) {
2477
+ if (Array.isArray(data)) {
2478
+ return data;
2479
+ }
2480
+ const rows = [];
2481
+ for (const [key, value] of Object.entries(data)) {
2482
+ const row = value;
2483
+ if (row !== null && typeof row === "object" && (row[primaryKey] === void 0 || row[primaryKey] === null)) {
2484
+ row[primaryKey] = key;
2485
+ }
2486
+ rows.push(value);
2487
+ }
2488
+ return rows;
2489
+ }
2490
+ };
2491
+
2492
+ // src/module/BaseModule.ts
2493
+ var BaseModule = class {
2494
+ /** App 门面:模块内访问网络/存储/事件等的统一入口 */
2495
+ get app() {
2496
+ return App;
2497
+ }
2498
+ /**
2499
+ * 初始化。ModuleRegistry.initAll 按注册顺序调用,可返回 Promise 以等待
2500
+ * 异步初始化(读存档、拉服务端配置等)完成后再初始化下一个模块。
2501
+ * 默认空实现,子类按需覆写。
2502
+ */
2503
+ init() {
2504
+ }
2505
+ /**
2506
+ * 释放。ModuleRegistry.disposeAll 调用,清理订阅 / 定时器 / 缓存等。
2507
+ * 默认空实现,子类按需覆写。
2508
+ */
2509
+ dispose() {
2510
+ }
2511
+ };
2512
+
2513
+ // src/module/ModuleRegistry.ts
2514
+ var ModuleRegistry = class {
2515
+ constructor() {
2516
+ /** name → 模块实例 */
2517
+ __publicField(this, "modules", /* @__PURE__ */ new Map());
2518
+ /** 注册顺序(initAll 正序、disposeAll 逆序据此遍历) */
2519
+ __publicField(this, "order", []);
2520
+ }
2521
+ /**
2522
+ * 注册一个模块。name 重复将抛错(避免静默覆盖导致取到错误实例)。
2523
+ */
2524
+ register(module) {
2525
+ const name = module.name;
2526
+ if (!name) {
2527
+ throw new Error("[ModuleRegistry] \u6A21\u5757\u7F3A\u5C11 name,\u65E0\u6CD5\u6CE8\u518C\u3002");
2528
+ }
2529
+ if (this.modules.has(name)) {
2530
+ throw new Error(`[ModuleRegistry] \u6A21\u5757\u540D\u91CD\u590D\u6CE8\u518C: ${name}`);
2531
+ }
2532
+ this.modules.set(name, module);
2533
+ this.order.push(name);
2534
+ }
2535
+ /**
2536
+ * 按注册顺序依次初始化所有模块,await 异步 init。
2537
+ * 串行(非并行)以尊重模块间的初始化依赖顺序。
2538
+ */
2539
+ async initAll() {
2540
+ for (const name of this.order) {
2541
+ const module = this.modules.get(name);
2542
+ if (module) {
2543
+ await module.init();
2544
+ }
2545
+ }
2546
+ }
2547
+ /**
2548
+ * 释放所有模块。按逆注册顺序释放(后注册者先释放),
2549
+ * 单个模块 dispose 抛错不阻断其余模块释放。
2550
+ */
2551
+ disposeAll() {
2552
+ for (let i = this.order.length - 1; i >= 0; i--) {
2553
+ const module = this.modules.get(this.order[i]);
2554
+ if (!module) continue;
2555
+ try {
2556
+ module.dispose();
2557
+ } catch (e) {
2558
+ console.warn(`[ModuleRegistry] \u6A21\u5757 ${module.name} dispose \u5931\u8D25:`, e?.message ?? e);
2559
+ }
2560
+ }
2561
+ this.modules.clear();
2562
+ this.order.length = 0;
2563
+ }
2564
+ /**
2565
+ * 按 name 取模块,取不到抛清晰错误(避免业务拿到 undefined 后在更深处崩溃)。
2566
+ * 泛型 T 便于直接拿到具体模块类型,无需手动断言。
2567
+ */
2568
+ get(name) {
2569
+ const module = this.modules.get(name);
2570
+ if (!module) {
2571
+ throw new Error(
2572
+ `[ModuleRegistry] \u672A\u627E\u5230\u6A21\u5757: ${name}\u3002\u8BF7\u786E\u8BA4\u5DF2 register(new XxxModule()) \u4E14 name \u4E00\u81F4\u3002`
2573
+ );
2574
+ }
2575
+ return module;
2576
+ }
2577
+ /** 指定 name 的模块是否已注册 */
2578
+ has(name) {
2579
+ return this.modules.has(name);
2580
+ }
2581
+ /** 已注册模块数量 */
2582
+ get size() {
2583
+ return this.modules.size;
2584
+ }
2585
+ };
2586
+ var BasePanel = class extends Phaser4.GameObjects.Container {
2587
+ constructor(scene) {
2588
+ super(scene, 0, 0);
2589
+ /** 打开本面板时所属的 key(由 PanelManager 在创建后注入,用于自我关闭) */
2590
+ __publicField(this, "_key", "");
2591
+ /** 管理本面板的 PanelManager(由其在创建后注入) */
2592
+ __publicField(this, "_manager", null);
2593
+ /** open 时透传进来的业务数据 */
2594
+ __publicField(this, "_data");
2595
+ /** 关闭时需要逐一执行的清理句柄(订阅、定时器等) */
2596
+ __publicField(this, "disposers", []);
2597
+ /** 防止重复关闭 */
2598
+ __publicField(this, "closing", false);
2599
+ }
2600
+ /** App 门面:面板内访问网络/音频/存储/事件等的统一入口 */
2601
+ get app() {
2602
+ return App;
2603
+ }
2604
+ /**
2605
+ * open 时传入的业务数据(子类在 onCreate/onShow 中读取)。
2606
+ * 注意:Phaser.Container 自身的 `data` 字段是其内置 DataManager,
2607
+ * 故业务数据用 `panelData` 暴露,避免与基类字段冲突。
2608
+ */
2609
+ get panelData() {
2610
+ return this._data;
2611
+ }
2612
+ /** 本面板的注册 key */
2613
+ get key() {
2614
+ return this._key;
2615
+ }
2616
+ /* ----------------------------- 生命周期 ----------------------------- */
2617
+ /**
2618
+ * 构建 UI。子类覆写,在此创建并 add 所有子对象。
2619
+ * 可返回 Promise(异步加载资源/数据时),PanelManager 会 await 后再 onShow。
2620
+ * @param data open 时透传的业务数据(等价于 this.data)
2621
+ */
2622
+ onCreate(_data) {
2623
+ }
2624
+ /** 显示时回调(入场动画 / 数据刷新)。子类按需覆写。 */
2625
+ onShow() {
2626
+ }
2627
+ /** 隐藏时回调(退场动画)。子类按需覆写。在 onClose 之前调用。 */
2628
+ onHide() {
2629
+ }
2630
+ /**
2631
+ * 释放回调。默认实现:执行全部 bind 登记的清理 + 销毁容器。
2632
+ * 子类如需自定义释放逻辑,覆写后请调用 super.onClose() 以保证基础清理执行。
2633
+ */
2634
+ onClose() {
2635
+ this.runDisposers();
2636
+ if (!this.destroyed) {
2637
+ this.destroy(true);
2638
+ }
2639
+ }
2640
+ /* ----------------------------- 对外能力 ----------------------------- */
2641
+ /**
2642
+ * 登记一个需在面板关闭时自动撤销的句柄(事件订阅返回的 Unsubscribe、
2643
+ * 自定义清理函数等)。基类 onClose 会统一执行,业务无需手动记账。
2644
+ */
2645
+ bind(unsub) {
2646
+ this.disposers.push(unsub);
2647
+ }
2648
+ /** 关闭自己(委托给 PanelManager 走完整的 onHide → onClose → 出栈)。 */
2649
+ close() {
2650
+ if (this.closing) return;
2651
+ if (this._manager) {
2652
+ this._manager.close(this._key);
2653
+ } else {
2654
+ this.__hide();
2655
+ this.__close();
2656
+ }
2657
+ }
2658
+ /* ----------------------------- 辅助 UI ----------------------------- */
2659
+ /**
2660
+ * 添加一层覆盖全屏的半透明遮罩(放在面板最底层),点击可选触发关闭。
2661
+ * 以设计稿尺寸铺满;遮罩本身加入容器,随面板一起销毁。
2662
+ * @param opts.color 遮罩颜色(默认黑)
2663
+ * @param opts.alpha 不透明度(默认 0.6)
2664
+ * @param opts.closeOnClick 点击遮罩是否关闭面板(默认 false)
2665
+ */
2666
+ addDim(opts) {
2667
+ const w = this.designWidth;
2668
+ const h = this.designHeight;
2669
+ const color = opts?.color ?? 0;
2670
+ const alpha = opts?.alpha ?? 0.6;
2671
+ const dim = this.scene.make.graphics({}, false);
2672
+ dim.fillStyle(color, alpha);
2673
+ dim.fillRect(0, 0, w, h);
2674
+ if (opts?.closeOnClick) {
2675
+ dim.setInteractive(new Phaser4.Geom.Rectangle(0, 0, w, h), Phaser4.Geom.Rectangle.Contains);
2676
+ dim.on(Phaser4.Input.Events.POINTER_UP, () => this.close());
2677
+ }
2678
+ this.addAt(dim, 0);
2679
+ return dim;
2680
+ }
2681
+ /**
2682
+ * 把一个子对象放到设计稿正中(以设计稿坐标系)。
2683
+ * 仅设置坐标,不负责 add(由调用方决定何时 add 进容器)。
2684
+ */
2685
+ centerChild(child) {
2686
+ child.setPosition(this.designWidth / 2, this.designHeight / 2);
2687
+ }
2688
+ /** 设计稿宽度(来自 App.config) */
2689
+ get designWidth() {
2690
+ return this.app.config.value.designWidth;
2691
+ }
2692
+ /** 设计稿高度(来自 App.config) */
2693
+ get designHeight() {
2694
+ return this.app.config.value.designHeight;
2695
+ }
2696
+ /* --------------------- 仅供 PanelManager 调用的内部钩子 --------------------- */
2697
+ /**
2698
+ * PanelManager 创建面板后注入归属信息。下划线前缀表示"框架内部使用",业务勿调。
2699
+ */
2700
+ __attach(manager, key, data) {
2701
+ this._manager = manager;
2702
+ this._key = key;
2703
+ this._data = data;
2704
+ }
2705
+ /** 触发 onCreate(可能异步)。仅 PanelManager 调用。 */
2706
+ __create() {
2707
+ return this.onCreate(this._data);
2708
+ }
2709
+ /** 触发 onShow。仅 PanelManager 调用。 */
2710
+ __show() {
2711
+ this.onShow();
2712
+ }
2713
+ /** 触发 onHide。仅 PanelManager 调用。 */
2714
+ __hide() {
2715
+ this.onHide();
2716
+ }
2717
+ /** 触发 onClose(释放)。仅 PanelManager 调用,带防重入。 */
2718
+ __close() {
2719
+ if (this.closing) return;
2720
+ this.closing = true;
2721
+ this.onClose();
2722
+ }
2723
+ /** 逐一执行并清空已登记的清理句柄(单个失败不影响其余)。 */
2724
+ runDisposers() {
2725
+ for (const dispose of this.disposers) {
2726
+ try {
2727
+ dispose();
2728
+ } catch (e) {
2729
+ this.app.log.tag("Panel").warn("\u9762\u677F\u6E05\u7406\u53E5\u67C4\u6267\u884C\u5931\u8D25", e?.message ?? e);
2730
+ }
2731
+ }
2732
+ this.disposers.length = 0;
2733
+ }
2734
+ /** 容器是否已被销毁(Phaser 销毁后 scene 引用会被清空) */
2735
+ get destroyed() {
2736
+ return this.scene == null;
2737
+ }
2738
+ };
2739
+
2740
+ // src/ui/panel/layers.ts
2741
+ var Layers = {
2742
+ /** 场景背景层 */
2743
+ Scene: 0,
2744
+ /** 实体层(角色、道具、子弹等) */
2745
+ Entity: 100,
2746
+ /** 常驻 HUD 层 */
2747
+ Hud: 1e3,
2748
+ /** 弹出面板层(PanelManager 管理) */
2749
+ Panel: 2e3,
2750
+ /** 轻提示 / Toast 层 */
2751
+ Toast: 3e3,
2752
+ /** 加载 / 转场遮罩层 */
2753
+ Loading: 4e3,
2754
+ /** 新手引导层(最顶) */
2755
+ Guide: 5e3
2756
+ };
2757
+
2758
+ // src/ui/panel/PanelManager.ts
2759
+ var PanelManager = class {
2760
+ constructor() {
2761
+ /** key → 构造器 */
2762
+ __publicField(this, "registry", /* @__PURE__ */ new Map());
2763
+ /** 打开中的面板栈(后进者在数组尾,即栈顶) */
2764
+ __publicField(this, "stack", []);
2765
+ /** bootstrap 注入的 Phaser.Game,用于取当前活动场景 */
2766
+ __publicField(this, "game", null);
2767
+ }
2768
+ /** 由 bootstrap 在 Phaser.Game 创建后调用 */
2769
+ bindGame(game) {
2770
+ this.game = game;
2771
+ }
2772
+ register(key, ctor) {
2773
+ if (this.registry.has(key)) {
2774
+ console.warn(`[PanelManager] \u9762\u677F key \u91CD\u590D\u6CE8\u518C,\u540E\u8005\u8986\u76D6\u524D\u8005: ${key}`);
2775
+ }
2776
+ this.registry.set(key, ctor);
2777
+ }
2778
+ /**
2779
+ * 打开面板。流程:
2780
+ * 取栈顶活动场景 → new 面板 → 加入场景显示列表 → setDepth(Layers.Panel + 栈深)
2781
+ * → await onCreate → onShow → 压栈
2782
+ * 若同 key 已打开,直接返回已有实例(不重复创建)。
2783
+ */
2784
+ async open(key, data) {
2785
+ const existing = this.find(key);
2786
+ if (existing) {
2787
+ return existing.panel;
2788
+ }
2789
+ const ctor = this.registry.get(key);
2790
+ if (!ctor) {
2791
+ throw new Error(`[PanelManager] \u672A\u6CE8\u518C\u7684\u9762\u677F key: ${key}\u3002\u8BF7\u5148 register('${key}', SomePanel)\u3002`);
2792
+ }
2793
+ const scene = this.activeScene();
2794
+ if (!scene) {
2795
+ throw new Error(
2796
+ `[PanelManager] \u6CA1\u6709\u53EF\u7528\u7684\u6D3B\u52A8\u573A\u666F,\u65E0\u6CD5\u6253\u5F00\u9762\u677F ${key}\u3002\u8BF7\u786E\u8BA4\u5DF2 bindGame \u4E14\u81F3\u5C11\u6709\u4E00\u4E2A\u573A\u666F\u5728\u8FD0\u884C\u3002`
2797
+ );
2798
+ }
2799
+ const panel = new ctor(scene);
2800
+ panel.__attach(this, key, data);
2801
+ scene.add.existing(panel);
2802
+ panel.setDepth(Layers.Panel + this.stack.length);
2803
+ this.stack.push({ key, panel });
2804
+ await panel.__create();
2805
+ panel.__show();
2806
+ return panel;
2807
+ }
2808
+ close(key) {
2809
+ const idx = this.stack.findIndex((e) => e.key === key);
2810
+ if (idx < 0) return;
2811
+ const [entry] = this.stack.splice(idx, 1);
2812
+ this.teardown(entry.panel);
2813
+ this.reindexDepth();
2814
+ }
2815
+ closeTop() {
2816
+ const top = this.stack[this.stack.length - 1];
2817
+ if (!top) return;
2818
+ this.stack.pop();
2819
+ this.teardown(top.panel);
2820
+ }
2821
+ closeAll() {
2822
+ while (this.stack.length > 0) {
2823
+ const entry = this.stack.pop();
2824
+ this.teardown(entry.panel);
2825
+ }
2826
+ }
2827
+ isOpen(key) {
2828
+ return this.find(key) !== void 0;
2829
+ }
2830
+ /** 当前打开的面板数量 */
2831
+ get stackSize() {
2832
+ return this.stack.length;
2833
+ }
2834
+ /* ------------------------------ 内部 ------------------------------ */
2835
+ /** 执行一个面板的 onHide → onClose(释放) */
2836
+ teardown(panel) {
2837
+ panel.__hide();
2838
+ panel.__close();
2839
+ }
2840
+ /** 关闭非栈顶面板后,按当前栈顺序重排剩余面板的 depth */
2841
+ reindexDepth() {
2842
+ for (let i = 0; i < this.stack.length; i++) {
2843
+ this.stack[i].panel.setDepth(Layers.Panel + i);
2844
+ }
2845
+ }
2846
+ find(key) {
2847
+ return this.stack.find((e) => e.key === key);
2848
+ }
2849
+ /**
2850
+ * 取"当前活动场景"作为面板宿主:用 game.scene.getScenes(true)(运行中的场景)
2851
+ * 的最上层(数组末尾,渲染序最高)那个。
2852
+ *
2853
+ * 扩展点:若业务需要把面板固定挂到某个专用 UI 场景(而非当前场景),
2854
+ * 可在此处改为按指定 sceneKey 取场景,或给 open 增加 targetScene 参数后在此分发。
2855
+ */
2856
+ activeScene() {
2857
+ const manager = this.game?.scene;
2858
+ if (!manager) return null;
2859
+ const actives = manager.getScenes(true);
2860
+ if (actives.length === 0) return null;
2861
+ return actives[actives.length - 1];
2862
+ }
2863
+ };
2864
+
2865
+ // src/bootstrap.ts
2866
+ var _game = null;
2867
+ async function startGame(opts) {
2868
+ const { platform, config, scenes } = opts;
2869
+ PlatformContext.set(platform);
2870
+ await platform.init();
2871
+ const net = new NetManager({ apiBaseURL: config.apiBaseURL });
2872
+ const events = new EventBus();
2873
+ const log = new Logger(config.production);
2874
+ const configService = new ConfigService(config);
2875
+ const storage = new StorageManager();
2876
+ const audio = new AudioManager();
2877
+ const assets = new AssetLoader();
2878
+ const ads = new AdsManager(config.ads);
2879
+ const payment = new PaymentManager();
2880
+ const analytics = new AnalyticsManager();
2881
+ const account = new AccountService();
2882
+ const sceneManager = new SceneManager();
2883
+ const tables = new ConfigTableManager();
2884
+ const save = new SaveManager();
2885
+ const modules = new ModuleRegistry();
2886
+ const panelManager = new PanelManager();
2887
+ const i18n = new I18n({
2888
+ locale: normalizeLocale(config.defaultLocale ?? platform.info.system.language),
2889
+ fallbackLocale: config.i18n?.fallbackLocale ?? "en-US"
2890
+ });
2891
+ const lifecycle = platform.lifecycle;
2892
+ analytics.configureBackend({
2893
+ client: net.defaultHttp,
2894
+ // D4:装配阶段默认关闭,下面据 config.analytics.backendEnabled 决定是否启用;平台原生埋点不受影响
2895
+ enabled: false,
2896
+ reportPath: config.analytics?.reportPath,
2897
+ onError: (e) => log.tag("Analytics").warn("\u540E\u7AEF\u4E0A\u62A5\u5931\u8D25", e.message)
2898
+ });
2899
+ if (config.analytics?.backendEnabled) {
2900
+ analytics.enableBackend({ reportPath: config.analytics.reportPath });
2901
+ }
2902
+ const parts2 = {
2903
+ platform,
2904
+ net,
2905
+ events,
2906
+ log,
2907
+ config: configService,
2908
+ audio,
2909
+ storage,
2910
+ assets,
2911
+ ads,
2912
+ payment,
2913
+ analytics,
2914
+ account,
2915
+ scenes: sceneManager,
2916
+ i18n,
2917
+ save,
2918
+ tables,
2919
+ panels: panelManager,
2920
+ modules,
2921
+ lifecycle
2922
+ };
2923
+ __initApp(parts2);
2924
+ if (opts.setup) {
2925
+ await opts.setup(App);
2926
+ }
2927
+ log.info(`[bootstrap] \u5E73\u53F0 ${platform.info.name} \u521D\u59CB\u5316\u5B8C\u6210,\u542F\u52A8 Phaser`);
2928
+ _game = await createPhaserGame(config, scenes);
2929
+ audio.bindGame(_game);
2930
+ sceneManager.bindGame(_game);
2931
+ panelManager.bindGame(_game);
2932
+ await App.modules.initAll();
2933
+ log.info("[bootstrap] \u6E38\u620F\u542F\u52A8\u5B8C\u6210");
2934
+ }
2935
+ function createPhaserGame(config, scenes) {
2936
+ return new Promise((resolve) => {
2937
+ const gameConfig = {
2938
+ type: Phaser4.AUTO,
2939
+ // 透明背景占位;业务场景自行绘制背景
2940
+ backgroundColor: "#1f2937",
2941
+ scale: {
2942
+ // FIT:按设计分辨率等比缩放铺满容器(保持比例,留黑边);CENTER_BOTH:居中
2943
+ mode: Phaser4.Scale.FIT,
2944
+ autoCenter: Phaser4.Scale.CENTER_BOTH,
2945
+ width: config.designWidth,
2946
+ height: config.designHeight
2947
+ },
2948
+ scene: scenes
2949
+ // 微信等无 DOM 平台由 weapp-adapter 提供 canvas;有 DOM 平台 Phaser 自动建 canvas
2950
+ };
2951
+ const game = new Phaser4.Game(gameConfig);
2952
+ game.events.once(Phaser4.Core.Events.READY, () => resolve(game));
2953
+ });
2954
+ }
2955
+
2956
+ // src/schedule/Scheduler.ts
2957
+ var HandleImpl = class {
2958
+ /**
2959
+ * @param dispose 真正的底层清理(clearTimeout/clearInterval/标记 nextTick 失效)
2960
+ * @param onCancel 通知 Scheduler 从跟踪集合移除
2961
+ */
2962
+ constructor(dispose, onCancel) {
2963
+ __publicField(this, "dispose", dispose);
2964
+ __publicField(this, "onCancel", onCancel);
2965
+ __publicField(this, "cancelled", false);
2966
+ }
2967
+ cancel() {
2968
+ if (this.cancelled) return;
2969
+ this.cancelled = true;
2970
+ this.dispose();
2971
+ this.onCancel(this);
2972
+ }
2973
+ /** 是否已取消(供 nextTick 在 microtask 触发前自检) */
2974
+ get isCancelled() {
2975
+ return this.cancelled;
2976
+ }
2977
+ };
2978
+ var Scheduler = class {
2979
+ constructor() {
2980
+ /** 所有未结束的句柄;clearAll 时统一取消 */
2981
+ __publicField(this, "handles", /* @__PURE__ */ new Set());
2982
+ }
2983
+ /** 延迟 ms 后执行一次 fn */
2984
+ delay(ms, fn) {
2985
+ let handle;
2986
+ const timer = setTimeout(() => {
2987
+ this.handles.delete(handle);
2988
+ fn();
2989
+ }, ms);
2990
+ handle = new HandleImpl(
2991
+ () => clearTimeout(timer),
2992
+ (h) => this.handles.delete(h)
2993
+ );
2994
+ this.handles.add(handle);
2995
+ return handle;
2996
+ }
2997
+ /** 每隔 ms 周期执行 fn,直到 cancel/clearAll */
2998
+ interval(ms, fn) {
2999
+ let handle;
3000
+ const timer = setInterval(() => {
3001
+ fn();
3002
+ }, ms);
3003
+ handle = new HandleImpl(
3004
+ () => clearInterval(timer),
3005
+ (h) => this.handles.delete(h)
3006
+ );
3007
+ this.handles.add(handle);
3008
+ return handle;
3009
+ }
3010
+ /** 下一个微任务(或 setTimeout 0)执行一次 fn */
3011
+ nextTick(fn) {
3012
+ let handle;
3013
+ const hasMicrotask = typeof queueMicrotask === "function" || typeof Promise !== "undefined";
3014
+ if (hasMicrotask) {
3015
+ handle = new HandleImpl(
3016
+ // microtask 无法真正撤销,靠 isCancelled 标志在触发时短路
3017
+ () => {
3018
+ },
3019
+ (h) => this.handles.delete(h)
3020
+ );
3021
+ const run = () => {
3022
+ if (handle.isCancelled) return;
3023
+ this.handles.delete(handle);
3024
+ fn();
3025
+ };
3026
+ if (typeof queueMicrotask === "function") {
3027
+ queueMicrotask(run);
3028
+ } else {
3029
+ Promise.resolve().then(run);
3030
+ }
3031
+ } else {
3032
+ const timer = setTimeout(() => {
3033
+ this.handles.delete(handle);
3034
+ fn();
3035
+ }, 0);
3036
+ handle = new HandleImpl(
3037
+ () => clearTimeout(timer),
3038
+ (h) => this.handles.delete(h)
3039
+ );
3040
+ }
3041
+ this.handles.add(handle);
3042
+ return handle;
3043
+ }
3044
+ /** 取消单个句柄(等价于 handle.cancel(),幂等) */
3045
+ clear(handle) {
3046
+ handle.cancel();
3047
+ }
3048
+ /** 取消并清空所有句柄 */
3049
+ clearAll() {
3050
+ for (const h of Array.from(this.handles)) {
3051
+ h.cancel();
3052
+ }
3053
+ this.handles.clear();
3054
+ }
3055
+ };
3056
+
3057
+ // src/scene/BaseScene.ts
3058
+ var BaseScene = class extends Phaser4.Scene {
3059
+ /**
3060
+ * E4 构造时挂上场景生命周期清理钩子。
3061
+ *
3062
+ * Phaser 场景实例可被复用(stop → start 重启):重启时不会再走 constructor,
3063
+ * 但会再次触发 create。因此清理逻辑必须在每次 SHUTDOWN 都执行,并在执行后
3064
+ * 重新登记(re-arm),保证下一轮生命周期同样被覆盖;DESTROY(实例被移除)只需一次。
3065
+ */
3066
+ constructor(config) {
3067
+ super(config);
3068
+ /* --------------------------- 自动清理 / 调度 --------------------------- */
3069
+ /**
3070
+ * 场景级定时器(E4)。
3071
+ * 业务在场景内用 this.scheduler.delay/interval/nextTick 启动定时,
3072
+ * 场景 shutdown / destroy 时框架自动 clearAll(),避免"幽灵回调"。
3073
+ */
3074
+ __publicField(this, "scheduler", new Scheduler());
3075
+ /**
3076
+ * 需在场景关闭时清理的订阅句柄登记表(E4)。
3077
+ * 收集 App.events.on / socket.onMessage / platform.lifecycle.onShow 等返回的 Unsubscribe,
3078
+ * 场景 shutdown / destroy 时统一执行并清空,避免订阅泄漏。
3079
+ */
3080
+ __publicField(this, "_bindings", []);
3081
+ /** 清理钩子是否已注册(防止同一实例重复注册) */
3082
+ __publicField(this, "_cleanupArmed", false);
3083
+ this.armCleanupHooks();
3084
+ }
3085
+ /** App 门面:业务在场景内访问网络/音频/存储等的统一入口 */
3086
+ get app() {
3087
+ return App;
3088
+ }
3089
+ /**
3090
+ * 登记需在场景关闭时清理的订阅(E4)。
3091
+ * @param unsub App.events.on / socket.onMessage 等返回的 Unsubscribe
3092
+ *
3093
+ * 用法:this.bind(this.app.events.on('xxx', handler));
3094
+ */
3095
+ bind(unsub) {
3096
+ this._bindings.push(unsub);
3097
+ }
3098
+ /** 注册 SHUTDOWN / DESTROY 清理钩子;SHUTDOWN 用 once + 自重注册以覆盖场景重启 */
3099
+ armCleanupHooks() {
3100
+ if (this._cleanupArmed) return;
3101
+ this._cleanupArmed = true;
3102
+ const onShutdown = () => {
3103
+ this.runCleanup();
3104
+ this.events.once(Phaser4.Scenes.Events.SHUTDOWN, onShutdown);
3105
+ };
3106
+ this.events.once(Phaser4.Scenes.Events.SHUTDOWN, onShutdown);
3107
+ this.events.once(Phaser4.Scenes.Events.DESTROY, () => this.runCleanup());
3108
+ }
3109
+ /** 执行全部清理:解绑所有 bind 的订阅 + 清空场景调度器,并重置登记表 */
3110
+ runCleanup() {
3111
+ for (const unsub of this._bindings) {
3112
+ try {
3113
+ unsub();
3114
+ } catch {
3115
+ }
3116
+ }
3117
+ this._bindings.length = 0;
3118
+ this.scheduler.clearAll();
3119
+ }
3120
+ /* --------------------------- 设计分辨率 --------------------------- */
3121
+ /** 设计稿宽度(来自 App.config) */
3122
+ get designWidth() {
3123
+ return this.app.config.value.designWidth;
3124
+ }
3125
+ /** 设计稿高度(来自 App.config) */
3126
+ get designHeight() {
3127
+ return this.app.config.value.designHeight;
3128
+ }
3129
+ /** 设计稿水平中心 X */
3130
+ get centerX() {
3131
+ return this.designWidth / 2;
3132
+ }
3133
+ /** 设计稿垂直中心 Y */
3134
+ get centerY() {
3135
+ return this.designHeight / 2;
3136
+ }
3137
+ /* ----------------------------- 安全区 ----------------------------- */
3138
+ /** 当前平台安全区(刘海/胶囊规避用),来自 App.platform.info.safeArea */
3139
+ get safeArea() {
3140
+ return this.app.platform.info.safeArea;
3141
+ }
3142
+ get safeTop() {
3143
+ return this.safeArea.top;
3144
+ }
3145
+ get safeBottom() {
3146
+ return this.safeArea.bottom;
3147
+ }
3148
+ get safeLeft() {
3149
+ return this.safeArea.left;
3150
+ }
3151
+ get safeRight() {
3152
+ return this.safeArea.right;
3153
+ }
3154
+ /**
3155
+ * 安全区内的可用矩形(以设计稿坐标系表示)。
3156
+ * 业务把关键 UI(返回按钮、顶栏)摆进这个矩形内即可避开遮挡。
3157
+ */
3158
+ get safeRect() {
3159
+ return {
3160
+ x: this.safeLeft,
3161
+ y: this.safeTop,
3162
+ width: this.designWidth - this.safeLeft - this.safeRight,
3163
+ height: this.designHeight - this.safeTop - this.safeBottom
3164
+ };
3165
+ }
3166
+ /* ----------------------------- 工具 ----------------------------- */
3167
+ /**
3168
+ * 计算把内容(srcW × srcH)等比缩放以"填满"目标框(dstW × dstH)的缩放系数。
3169
+ * 用于背景图铺满、保持比例的图标缩放等。
3170
+ */
3171
+ scaleToFill(srcW, srcH, dstW, dstH) {
3172
+ return Math.max(dstW / srcW, dstH / srcH);
3173
+ }
3174
+ /**
3175
+ * 计算把内容等比缩放以"放进"目标框(不裁切)的缩放系数。
3176
+ */
3177
+ scaleToFit(srcW, srcH, dstW, dstH) {
3178
+ return Math.min(dstW / srcW, dstH / srcH);
3179
+ }
3180
+ };
3181
+ var DEFAULTS = {
3182
+ width: 240,
3183
+ height: 88,
3184
+ backgroundColor: 3900150,
3185
+ backgroundColorActive: 2450411,
3186
+ radius: 16,
3187
+ textColor: "#ffffff",
3188
+ fontSize: 32,
3189
+ fontFamily: "sans-serif"
3190
+ };
3191
+ var Button = class extends Phaser4.GameObjects.Container {
3192
+ constructor(scene, config) {
3193
+ super(scene, config.x ?? 0, config.y ?? 0);
3194
+ __publicField(this, "bg");
3195
+ __publicField(this, "label");
3196
+ __publicField(this, "style");
3197
+ __publicField(this, "onClick");
3198
+ /** 是否禁用 */
3199
+ __publicField(this, "_disabled", false);
3200
+ /** 指针是否在按下且仍停留在按钮内 */
3201
+ __publicField(this, "pressed", false);
3202
+ this.style = {
3203
+ width: config.width ?? DEFAULTS.width,
3204
+ height: config.height ?? DEFAULTS.height,
3205
+ backgroundColor: config.backgroundColor ?? DEFAULTS.backgroundColor,
3206
+ backgroundColorActive: config.backgroundColorActive ?? DEFAULTS.backgroundColorActive,
3207
+ radius: config.radius ?? DEFAULTS.radius,
3208
+ textColor: config.textColor ?? DEFAULTS.textColor,
3209
+ fontSize: config.fontSize ?? DEFAULTS.fontSize,
3210
+ fontFamily: config.fontFamily ?? DEFAULTS.fontFamily
3211
+ };
3212
+ this.onClick = config.onClick;
3213
+ this.bg = scene.make.graphics({}, false);
3214
+ this.drawBackground(this.style.backgroundColor);
3215
+ this.add(this.bg);
3216
+ this.label = scene.make.text(
3217
+ {
3218
+ x: 0,
3219
+ y: 0,
3220
+ text: config.text,
3221
+ style: {
3222
+ color: this.style.textColor,
3223
+ fontSize: `${this.style.fontSize}px`,
3224
+ fontFamily: this.style.fontFamily
3225
+ }
3226
+ },
3227
+ false
3228
+ );
3229
+ this.label.setOrigin(0.5, 0.5);
3230
+ this.add(this.label);
3231
+ this.setSize(this.style.width, this.style.height);
3232
+ this.setInteractive(
3233
+ new Phaser4.Geom.Rectangle(
3234
+ -this.style.width / 2,
3235
+ -this.style.height / 2,
3236
+ this.style.width,
3237
+ this.style.height
3238
+ ),
3239
+ Phaser4.Geom.Rectangle.Contains
3240
+ );
3241
+ this.bindEvents();
3242
+ scene.add.existing(this);
3243
+ }
3244
+ /** 设置/更新按钮文案 */
3245
+ setText(text) {
3246
+ this.label.setText(text);
3247
+ return this;
3248
+ }
3249
+ /** 设置禁用态:禁用时变灰且不响应点击 */
3250
+ setDisabled(disabled) {
3251
+ this._disabled = disabled;
3252
+ this.setAlpha(disabled ? 0.5 : 1);
3253
+ if (disabled) {
3254
+ this.disableInteractive();
3255
+ this.pressed = false;
3256
+ this.drawBackground(this.style.backgroundColor);
3257
+ this.setScale(1);
3258
+ } else {
3259
+ this.setInteractive();
3260
+ }
3261
+ return this;
3262
+ }
3263
+ get disabled() {
3264
+ return this._disabled;
3265
+ }
3266
+ /** 绘制圆角背景(以中心为原点) */
3267
+ drawBackground(color) {
3268
+ const { width, height, radius } = this.style;
3269
+ this.bg.clear();
3270
+ this.bg.fillStyle(color, 1);
3271
+ this.bg.fillRoundedRect(-width / 2, -height / 2, width, height, radius);
3272
+ }
3273
+ /** 绑定指针交互:悬停/按下/抬起,实现按下态与点击触发 */
3274
+ bindEvents() {
3275
+ this.on(Phaser4.Input.Events.POINTER_OVER, () => {
3276
+ if (this._disabled) return;
3277
+ this.drawBackground(this.style.backgroundColorActive);
3278
+ });
3279
+ this.on(Phaser4.Input.Events.POINTER_OUT, () => {
3280
+ if (this._disabled) return;
3281
+ this.pressed = false;
3282
+ this.drawBackground(this.style.backgroundColor);
3283
+ this.setScale(1);
3284
+ });
3285
+ this.on(Phaser4.Input.Events.POINTER_DOWN, () => {
3286
+ if (this._disabled) return;
3287
+ this.pressed = true;
3288
+ this.drawBackground(this.style.backgroundColorActive);
3289
+ this.setScale(0.96);
3290
+ });
3291
+ this.on(Phaser4.Input.Events.POINTER_UP, () => {
3292
+ if (this._disabled) return;
3293
+ this.setScale(1);
3294
+ this.drawBackground(this.style.backgroundColor);
3295
+ if (this.pressed) {
3296
+ this.pressed = false;
3297
+ this.onClick?.();
3298
+ }
3299
+ });
3300
+ }
3301
+ };
3302
+
3303
+ // src/store/Store.ts
3304
+ function deepClone2(value) {
3305
+ const sc = globalThis.structuredClone;
3306
+ if (typeof sc === "function") {
3307
+ return sc(value);
3308
+ }
3309
+ return JSON.parse(JSON.stringify(value));
3310
+ }
3311
+ function createStore(initial) {
3312
+ const initialSnapshot = deepClone2(initial);
3313
+ let state = deepClone2(initial);
3314
+ const subscribers = /* @__PURE__ */ new Set();
3315
+ function notify() {
3316
+ const snapshot = state;
3317
+ for (const fn of Array.from(subscribers)) {
3318
+ fn(snapshot);
3319
+ }
3320
+ }
3321
+ function get() {
3322
+ return state;
3323
+ }
3324
+ function set(patch) {
3325
+ const delta = typeof patch === "function" ? patch(state) : patch;
3326
+ state = Object.assign({}, state, delta);
3327
+ notify();
3328
+ }
3329
+ function subscribe(fn) {
3330
+ subscribers.add(fn);
3331
+ return () => {
3332
+ subscribers.delete(fn);
3333
+ };
3334
+ }
3335
+ function select(selector, fn, equals = Object.is) {
3336
+ let prev = selector(state);
3337
+ const listener = (s) => {
3338
+ const next = selector(s);
3339
+ if (!equals(next, prev)) {
3340
+ const old = prev;
3341
+ prev = next;
3342
+ fn(next, old);
3343
+ }
3344
+ };
3345
+ return subscribe(listener);
3346
+ }
3347
+ function reset() {
3348
+ state = deepClone2(initialSnapshot);
3349
+ notify();
3350
+ }
3351
+ return { get, set, subscribe, select, reset };
3352
+ }
3353
+
3354
+ // src/event/TypedEventBus.ts
3355
+ var TypedEventBus = class {
3356
+ constructor() {
3357
+ /** 事件名 → 订阅列表 */
3358
+ __publicField(this, "map", /* @__PURE__ */ new Map());
3359
+ }
3360
+ /**
3361
+ * 订阅事件,返回取消订阅句柄。
3362
+ */
3363
+ on(event, handler) {
3364
+ const h = handler;
3365
+ this.add(event, { original: h, actual: h });
3366
+ return () => this.off(event, handler);
3367
+ }
3368
+ /**
3369
+ * 订阅一次:触发后自动解绑。返回的句柄也可在触发前手动取消。
3370
+ */
3371
+ once(event, handler) {
3372
+ const original = handler;
3373
+ const wrapper = (payload) => {
3374
+ this.remove(event, original);
3375
+ original(payload);
3376
+ };
3377
+ this.add(event, { original, actual: wrapper });
3378
+ return () => this.off(event, handler);
3379
+ }
3380
+ /**
3381
+ * 取消订阅:按原始 handler 匹配移除(只移除第一处匹配,与多次 on 的语义对应)。
3382
+ */
3383
+ off(event, handler) {
3384
+ this.remove(event, handler);
3385
+ }
3386
+ /**
3387
+ * 触发事件。payload 类型由 TMap[K] 约束;void 事件可省略第二参。
3388
+ */
3389
+ emit(event, payload) {
3390
+ const subs = this.map.get(event);
3391
+ if (!subs || subs.length === 0) return;
3392
+ const snapshot = subs.slice();
3393
+ for (const sub of snapshot) {
3394
+ sub.actual(payload);
3395
+ }
3396
+ }
3397
+ /**
3398
+ * 清空订阅:传 event 只清该事件,不传清空全部。
3399
+ */
3400
+ clear(event) {
3401
+ if (event === void 0) {
3402
+ this.map.clear();
3403
+ } else {
3404
+ this.map.delete(event);
3405
+ }
3406
+ }
3407
+ /* ------------------------------ 内部 ------------------------------ */
3408
+ add(event, sub) {
3409
+ const list = this.map.get(event);
3410
+ if (list) {
3411
+ list.push(sub);
3412
+ } else {
3413
+ this.map.set(event, [sub]);
3414
+ }
3415
+ }
3416
+ remove(event, original) {
3417
+ const list = this.map.get(event);
3418
+ if (!list) return;
3419
+ const idx = list.findIndex((s) => s.original === original);
3420
+ if (idx >= 0) {
3421
+ list.splice(idx, 1);
3422
+ if (list.length === 0) this.map.delete(event);
3423
+ }
3424
+ }
3425
+ };
3426
+ function createTypedEventBus() {
3427
+ return new TypedEventBus();
3428
+ }
3429
+
3430
+ // src/pool/ObjectPool.ts
3431
+ var ObjectPool = class {
3432
+ /**
3433
+ * @param factory 创建新对象的工厂函数
3434
+ * @param opts.reset 归还时重置对象状态的回调
3435
+ * @param opts.initial 初始预创建数量
3436
+ * @param opts.max 空闲队列上限(超出归还时丢弃);默认不限制
3437
+ */
3438
+ constructor(factory, opts) {
3439
+ /** 创建新对象的工厂 */
3440
+ __publicField(this, "factory");
3441
+ /** 归还时的重置回调(清状态),可选 */
3442
+ __publicField(this, "resetFn");
3443
+ /** 空闲队列上限;超出则丢弃归还对象。<=0 视为不限制 */
3444
+ __publicField(this, "maxSize");
3445
+ /** 空闲对象栈(后进先出,缓存友好) */
3446
+ __publicField(this, "free", []);
3447
+ /** 已创建对象总数(含借出与空闲;被丢弃后相应减少) */
3448
+ __publicField(this, "created", 0);
3449
+ this.factory = factory;
3450
+ this.resetFn = opts?.reset;
3451
+ this.maxSize = opts?.max ?? Infinity;
3452
+ const initial = opts?.initial ?? 0;
3453
+ if (initial > 0) this.preallocate(initial);
3454
+ }
3455
+ /** 已创建对象总数(借出 + 空闲) */
3456
+ get size() {
3457
+ return this.created;
3458
+ }
3459
+ /** 当前空闲(可被 acquire 直接复用)的对象数 */
3460
+ get available() {
3461
+ return this.free.length;
3462
+ }
3463
+ /** 借出一个对象:优先复用空闲对象,无则新建 */
3464
+ acquire() {
3465
+ const o = this.free.pop();
3466
+ if (o !== void 0) return o;
3467
+ const created = this.factory();
3468
+ this.created++;
3469
+ return created;
3470
+ }
3471
+ /**
3472
+ * 归还一个对象。先 reset,再视空闲上限决定放回或丢弃。
3473
+ * 丢弃时该对象不再由池持有,created 相应减少。
3474
+ */
3475
+ release(o) {
3476
+ if (this.resetFn) this.resetFn(o);
3477
+ if (this.free.length >= this.maxSize) {
3478
+ if (this.created > 0) this.created--;
3479
+ return;
3480
+ }
3481
+ this.free.push(o);
3482
+ }
3483
+ /** 预创建 n 个空闲对象 */
3484
+ preallocate(n) {
3485
+ for (let i = 0; i < n; i++) {
3486
+ this.free.push(this.factory());
3487
+ this.created++;
3488
+ }
3489
+ }
3490
+ /** 清空空闲队列并把已创建总数归零(池放弃所有持有) */
3491
+ clear() {
3492
+ this.free.length = 0;
3493
+ this.created = 0;
3494
+ }
3495
+ };
3496
+
3497
+ // src/error/FrameworkError.ts
3498
+ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
3499
+ ErrorCode2[ErrorCode2["Unknown"] = 0] = "Unknown";
3500
+ ErrorCode2[ErrorCode2["Network"] = 1] = "Network";
3501
+ ErrorCode2[ErrorCode2["Timeout"] = 2] = "Timeout";
3502
+ ErrorCode2[ErrorCode2["HttpStatus"] = 3] = "HttpStatus";
3503
+ ErrorCode2[ErrorCode2["SocketClosed"] = 4] = "SocketClosed";
3504
+ ErrorCode2[ErrorCode2["Storage"] = 5] = "Storage";
3505
+ ErrorCode2[ErrorCode2["PlatformUnsupported"] = 6] = "PlatformUnsupported";
3506
+ ErrorCode2[ErrorCode2["Payment"] = 7] = "Payment";
3507
+ ErrorCode2[ErrorCode2["Ads"] = 8] = "Ads";
3508
+ ErrorCode2[ErrorCode2["Config"] = 9] = "Config";
3509
+ ErrorCode2[ErrorCode2["Save"] = 10] = "Save";
3510
+ return ErrorCode2;
3511
+ })(ErrorCode || {});
3512
+ var FrameworkError = class _FrameworkError extends Error {
3513
+ constructor(code, message, cause) {
3514
+ super(message);
3515
+ /** 错误分类码 */
3516
+ __publicField(this, "code");
3517
+ /** 原始底层异常(第三方/原生错误),可选 */
3518
+ __publicField(this, "cause");
3519
+ this.name = "FrameworkError";
3520
+ Object.setPrototypeOf(this, _FrameworkError.prototype);
3521
+ this.code = code;
3522
+ this.cause = cause;
3523
+ }
3524
+ /* ----------------------- 静态便捷工厂 ----------------------- */
3525
+ static network(message, cause) {
3526
+ return new _FrameworkError(1 /* Network */, message, cause);
3527
+ }
3528
+ static timeout(message, cause) {
3529
+ return new _FrameworkError(2 /* Timeout */, message, cause);
3530
+ }
3531
+ static httpStatus(message, cause) {
3532
+ return new _FrameworkError(3 /* HttpStatus */, message, cause);
3533
+ }
3534
+ static socketClosed(message, cause) {
3535
+ return new _FrameworkError(4 /* SocketClosed */, message, cause);
3536
+ }
3537
+ static storage(message, cause) {
3538
+ return new _FrameworkError(5 /* Storage */, message, cause);
3539
+ }
3540
+ static platformUnsupported(message, cause) {
3541
+ return new _FrameworkError(6 /* PlatformUnsupported */, message, cause);
3542
+ }
3543
+ static payment(message, cause) {
3544
+ return new _FrameworkError(7 /* Payment */, message, cause);
3545
+ }
3546
+ static ads(message, cause) {
3547
+ return new _FrameworkError(8 /* Ads */, message, cause);
3548
+ }
3549
+ static config(message, cause) {
3550
+ return new _FrameworkError(9 /* Config */, message, cause);
3551
+ }
3552
+ static save(message, cause) {
3553
+ return new _FrameworkError(10 /* Save */, message, cause);
3554
+ }
3555
+ static unknown(message, cause) {
3556
+ return new _FrameworkError(0 /* Unknown */, message, cause);
3557
+ }
3558
+ };
3559
+ function isFrameworkError(e) {
3560
+ return e instanceof FrameworkError;
3561
+ }
3562
+
3563
+ // src/net/message/MessageChannel.ts
3564
+ var MessageError = class extends Error {
3565
+ constructor(message) {
3566
+ super(message);
3567
+ this.name = "MessageError";
3568
+ }
3569
+ };
3570
+ var JsonCodec = class {
3571
+ encode(env) {
3572
+ return JSON.stringify(env);
3573
+ }
3574
+ decode(raw) {
3575
+ if (typeof raw !== "string") {
3576
+ throw new MessageError("[JsonCodec] \u4EC5\u652F\u6301 string \u6D88\u606F,\u6536\u5230\u4E8C\u8FDB\u5236\u8BF7\u6539\u7528\u4E8C\u8FDB\u5236 codec");
3577
+ }
3578
+ const obj = JSON.parse(raw);
3579
+ if (!obj || typeof obj.type !== "string") {
3580
+ throw new MessageError("[JsonCodec] \u975E\u6CD5\u6D88\u606F:\u7F3A\u5C11 type \u5B57\u6BB5");
3581
+ }
3582
+ return {
3583
+ type: obj.type,
3584
+ seq: typeof obj.seq === "number" ? obj.seq : void 0,
3585
+ payload: obj.payload
3586
+ };
3587
+ }
3588
+ };
3589
+ var MessageChannel = class {
3590
+ constructor(socket, opts = {}) {
3591
+ __publicField(this, "socket");
3592
+ __publicField(this, "codec");
3593
+ __publicField(this, "requestTimeoutMs");
3594
+ /** 递增请求序号(从 1 开始,0 留作"无 seq"语义之外的保留) */
3595
+ __publicField(this, "seqCounter", 0);
3596
+ /** 等待响应的请求:seq → pending */
3597
+ __publicField(this, "pending", /* @__PURE__ */ new Map());
3598
+ /** 按 type 路由的服务端推送监听器:type → 处理器集合 */
3599
+ __publicField(this, "typeListeners", /* @__PURE__ */ new Map());
3600
+ /** 重连(非首次 open)监听器 */
3601
+ __publicField(this, "reconnectListeners", /* @__PURE__ */ new Set());
3602
+ /** 是否已发生过首次 open(用于区分"首连"与"重连") */
3603
+ __publicField(this, "opened", false);
3604
+ /** 已注册到 socket 的事件取消句柄,close 时统一清理 */
3605
+ __publicField(this, "socketUnsubs", []);
3606
+ /** 是否已 close(关闭后拒绝新请求 / 不再派发) */
3607
+ __publicField(this, "closed", false);
3608
+ this.socket = socket;
3609
+ this.codec = opts.codec ?? new JsonCodec();
3610
+ this.requestTimeoutMs = opts.requestTimeoutMs ?? 1e4;
3611
+ this.bindSocket();
3612
+ }
3613
+ /* ------------------------------ 公开 API ------------------------------ */
3614
+ /** 建立连接(委托底层 socket.connect) */
3615
+ connect() {
3616
+ if (this.closed) {
3617
+ return Promise.reject(new MessageError("[MessageChannel] \u901A\u9053\u5DF2\u5173\u95ED,\u65E0\u6CD5 connect"));
3618
+ }
3619
+ return this.socket.connect();
3620
+ }
3621
+ /**
3622
+ * 发送请求并等待同 seq 响应。
3623
+ * @template TRes 期望响应 payload 类型
3624
+ * @param type 消息类型
3625
+ * @param payload 请求负载
3626
+ * @returns 服务端响应的 payload(按 TRes 断言)
3627
+ * @throws MessageError 超时 / 通道已关闭 / 发送失败
3628
+ */
3629
+ request(type, payload) {
3630
+ if (this.closed) {
3631
+ return Promise.reject(new MessageError("[MessageChannel] \u901A\u9053\u5DF2\u5173\u95ED,\u65E0\u6CD5 request"));
3632
+ }
3633
+ const seq = this.nextSeq();
3634
+ const env = { type, seq, payload };
3635
+ return new Promise((resolve, reject) => {
3636
+ const timer = this.requestTimeoutMs > 0 ? setTimeout(() => {
3637
+ this.pending.delete(seq);
3638
+ reject(
3639
+ new MessageError(
3640
+ `[MessageChannel] \u8BF7\u6C42\u8D85\u65F6(type='${type}', seq=${seq}, timeout=${this.requestTimeoutMs}ms)`
3641
+ )
3642
+ );
3643
+ }, this.requestTimeoutMs) : null;
3644
+ this.pending.set(seq, {
3645
+ resolve,
3646
+ reject,
3647
+ timer
3648
+ });
3649
+ try {
3650
+ this.socket.send(this.codec.encode(env));
3651
+ } catch (e) {
3652
+ this.clearPending(seq);
3653
+ reject(
3654
+ new MessageError(
3655
+ `[MessageChannel] \u53D1\u9001\u5931\u8D25(type='${type}'):` + (e instanceof Error ? e.message : String(e))
3656
+ )
3657
+ );
3658
+ }
3659
+ });
3660
+ }
3661
+ /** 发送单向消息(不带 seq,不等响应) */
3662
+ send(type, payload) {
3663
+ if (this.closed) {
3664
+ throw new MessageError("[MessageChannel] \u901A\u9053\u5DF2\u5173\u95ED,\u65E0\u6CD5 send");
3665
+ }
3666
+ const env = { type, payload };
3667
+ this.socket.send(this.codec.encode(env));
3668
+ }
3669
+ /**
3670
+ * 订阅某类型的服务端推送(无 seq 的消息,或带 seq 但无匹配 pending 的消息)。
3671
+ * @template T 推送 payload 类型
3672
+ * @returns 取消订阅句柄
3673
+ */
3674
+ on(type, handler) {
3675
+ let set = this.typeListeners.get(type);
3676
+ if (!set) {
3677
+ set = /* @__PURE__ */ new Set();
3678
+ this.typeListeners.set(type, set);
3679
+ }
3680
+ const h = handler;
3681
+ set.add(h);
3682
+ return () => {
3683
+ const s = this.typeListeners.get(type);
3684
+ if (s) {
3685
+ s.delete(h);
3686
+ if (s.size === 0) this.typeListeners.delete(type);
3687
+ }
3688
+ };
3689
+ }
3690
+ /**
3691
+ * 订阅"重连成功"事件:socket 首次 open 后,再次 open 时触发。
3692
+ * 业务在此重新鉴权 / 重新订阅 / 补偿状态。
3693
+ * @returns 取消订阅句柄
3694
+ */
3695
+ onReconnect(fn) {
3696
+ this.reconnectListeners.add(fn);
3697
+ return () => this.reconnectListeners.delete(fn);
3698
+ }
3699
+ /**
3700
+ * 关闭通道:关闭底层 socket、解绑事件、拒绝所有未决请求、清空监听器。
3701
+ * 关闭后本通道不可再用(再次 connect/request/send 会抛错)。
3702
+ */
3703
+ close() {
3704
+ if (this.closed) return;
3705
+ this.closed = true;
3706
+ for (const [seq, p] of this.pending) {
3707
+ if (p.timer !== null) clearTimeout(p.timer);
3708
+ p.reject(new MessageError(`[MessageChannel] \u901A\u9053\u5173\u95ED,\u8BF7\u6C42\u88AB\u53D6\u6D88(seq=${seq})`));
3709
+ }
3710
+ this.pending.clear();
3711
+ for (const unsub of this.socketUnsubs) {
3712
+ try {
3713
+ unsub();
3714
+ } catch {
3715
+ }
3716
+ }
3717
+ this.socketUnsubs.length = 0;
3718
+ this.typeListeners.clear();
3719
+ this.reconnectListeners.clear();
3720
+ try {
3721
+ this.socket.close();
3722
+ } catch {
3723
+ }
3724
+ }
3725
+ /* ------------------------------ 内部 ------------------------------ */
3726
+ /** 接线底层 socket 的 open / message 事件 */
3727
+ bindSocket() {
3728
+ this.socketUnsubs.push(
3729
+ this.socket.onOpen(() => this.handleOpen())
3730
+ );
3731
+ this.socketUnsubs.push(
3732
+ this.socket.onMessage((raw) => this.handleMessage(raw))
3733
+ );
3734
+ }
3735
+ /** socket open:首次记为"已连",之后每次都视为"重连"并触发 onReconnect */
3736
+ handleOpen() {
3737
+ if (!this.opened) {
3738
+ this.opened = true;
3739
+ return;
3740
+ }
3741
+ for (const fn of this.reconnectListeners) {
3742
+ try {
3743
+ fn();
3744
+ } catch {
3745
+ }
3746
+ }
3747
+ }
3748
+ /** 收到底层数据:解码 → 有 seq 命中 pending 则 resolve,否则按 type 派发 */
3749
+ handleMessage(raw) {
3750
+ if (this.closed) return;
3751
+ let env;
3752
+ try {
3753
+ env = this.codec.decode(raw);
3754
+ } catch {
3755
+ return;
3756
+ }
3757
+ if (typeof env.seq === "number") {
3758
+ const p = this.pending.get(env.seq);
3759
+ if (p) {
3760
+ this.clearPending(env.seq);
3761
+ p.resolve(env.payload);
3762
+ return;
3763
+ }
3764
+ }
3765
+ const set = this.typeListeners.get(env.type);
3766
+ if (set && set.size > 0) {
3767
+ for (const handler of set) {
3768
+ try {
3769
+ handler(env.payload);
3770
+ } catch {
3771
+ }
3772
+ }
3773
+ }
3774
+ }
3775
+ /** 取下一个请求序号 */
3776
+ nextSeq() {
3777
+ this.seqCounter += 1;
3778
+ return this.seqCounter;
3779
+ }
3780
+ /** 清理某个 pending(清超时定时器并从表中移除) */
3781
+ clearPending(seq) {
3782
+ const p = this.pending.get(seq);
3783
+ if (p) {
3784
+ if (p.timer !== null) clearTimeout(p.timer);
3785
+ this.pending.delete(seq);
3786
+ }
3787
+ }
3788
+ };
3789
+
3790
+ export { App, BaseModule, BasePanel, BaseScene, Button, ConfigTable, ConfigTableManager, ErrorCode, FrameworkError, HttpClient, HttpError, I18n, JsonCodec, KcpClient, Layers, LogLevel, MessageChannel, MessageError, ModuleRegistry, NetManager, ObjectPool, PanelManager, PlatformContext, SaveManager, Scheduler, TypedEventBus, WebSocketClient, createStore, createTypedEventBus, err, isFrameworkError, normalizeLocale, ok, startGame };