kadou-game-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1761 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var SDK_VERSION = '1.0.0';
6
+
7
+ /**
8
+ * 卡兜游戏 SDK 通讯消息类型定义
9
+ * 用于 父页面(Vue壳) <-> 子iframe(游戏) 之间的 postMessage 通讯
10
+ */
11
+ var MESSAGE_TYPES = {
12
+ // 基础系统
13
+ SDK_READY: 'SDK_READY',
14
+ HEARTBEAT_PING: 'HEARTBEAT_PING',
15
+ HEARTBEAT_PONG: 'HEARTBEAT_PONG',
16
+ ERROR_THROWN: 'ERROR_THROWN',
17
+ // 父 -> 子:游戏控制
18
+ GAME_LOAD: 'GAME_LOAD',
19
+ //加载游戏
20
+ GAME_START: 'GAME_START',
21
+ //游戏开始
22
+ GAME_PAUSE: 'GAME_PAUSE',
23
+ //游戏暂停
24
+ GAME_RESUME: 'GAME_RESUME',
25
+ //游戏恢复
26
+ GAME_RESTART: 'GAME_RESTART',
27
+ //重玩游戏
28
+ GAME_DESTROY: 'GAME_DESTROY',
29
+ //销毁游戏
30
+
31
+ // 子 -> 父:游戏状态
32
+ GAME_READY: 'GAME_READY',
33
+ //游戏准备完成
34
+ GAME_LOADED: 'GAME_LOADED',
35
+ //游戏加载完成
36
+ GAME_LOADING: 'GAME_LOADING',
37
+ //游戏加载进度
38
+ GAME_FINISH: 'GAME_FINISH',
39
+ //游戏结束
40
+ GAME_RANK: 'GAME_RANK',
41
+ //游戏排行榜
42
+
43
+ // 配置 / 主题
44
+ THEME_UPDATE: 'THEME_UPDATE',
45
+ //更新主题
46
+ CONFIG_UPDATE: 'CONFIG_UPDATE',
47
+ //配置更新
48
+
49
+ // 题目 / 答题
50
+ QUESTION_SUBMIT: 'QUESTION_SUBMIT',
51
+ //单题提交
52
+ QUESTION_PREV: 'QUESTION_PREV',
53
+ //上一题
54
+ QUESTION_NEXT: 'QUESTION_NEXT',
55
+ //下一题
56
+ ANSWER_START: 'ANSWER_START',
57
+ //开始答题
58
+ ANSWER_COMPLETED: 'ANSWER_COMPLETED',
59
+ //答题完成
60
+
61
+ // 音频
62
+ AUDIO_VOLUME_CHANGE: 'AUDIO_VOLUME_CHANGE',
63
+ //调节音量包括 音效背景音
64
+
65
+ //UI页面
66
+ PAGE_START_SHOW: 'PAGE_START_SHOW',
67
+ //显示开始画面
68
+ PAGE_END_SHOW: 'PAGE_END_SHOW',
69
+ //显示结束画面
70
+
71
+ EXTENSION_EVENT: 'EXTENSION_EVENT',
72
+ //扩展消息类型
73
+
74
+ /**
75
+ * 异常消息
76
+ */
77
+ ERROR: 'ERROR'
78
+ };
79
+
80
+ function isFunction(fn) {
81
+ return typeof fn === 'function';
82
+ }
83
+ function isObject(val) {
84
+ return Object.prototype.toString.call(val) === '[object Object]';
85
+ }
86
+ function createRequestId(prefix) {
87
+ prefix = prefix || 'msg';
88
+ return prefix + '_' + new Date().getTime() + '_' + Math.random().toString(16).slice(2, 10);
89
+ }
90
+
91
+ function EventEmitter() {
92
+ this.events = {};
93
+ }
94
+ EventEmitter.prototype.on = function (eventName, handler) {
95
+ if (!eventName || !isFunction(handler)) return this;
96
+ if (!this.events[eventName]) {
97
+ this.events[eventName] = [];
98
+ }
99
+ this.events[eventName].push(handler);
100
+ return this;
101
+ };
102
+ EventEmitter.prototype.once = function (eventName, handler) {
103
+ var self = this;
104
+ if (!eventName || !isFunction(handler)) return this;
105
+ function wrapper() {
106
+ self.off(eventName, wrapper);
107
+ handler.apply(null, arguments);
108
+ }
109
+ wrapper.__original__ = handler;
110
+ this.on(eventName, wrapper);
111
+ return this;
112
+ };
113
+ EventEmitter.prototype.off = function (eventName, handler) {
114
+ var handlers;
115
+ var result;
116
+ var i;
117
+ var fn;
118
+ if (!eventName) {
119
+ this.events = {};
120
+ return this;
121
+ }
122
+ handlers = this.events[eventName];
123
+ if (!handlers || !handlers.length) return this;
124
+ if (!handler) {
125
+ delete this.events[eventName];
126
+ return this;
127
+ }
128
+ result = [];
129
+ for (i = 0; i < handlers.length; i++) {
130
+ fn = handlers[i];
131
+ if (fn !== handler && fn.__original__ !== handler) {
132
+ result.push(fn);
133
+ }
134
+ }
135
+ if (result.length) {
136
+ this.events[eventName] = result;
137
+ } else {
138
+ delete this.events[eventName];
139
+ }
140
+ return this;
141
+ };
142
+ EventEmitter.prototype.emit = function (eventName) {
143
+ var handlers = this.events[eventName];
144
+ var args;
145
+ var i;
146
+ if (!handlers || !handlers.length) return this;
147
+ args = Array.prototype.slice.call(arguments, 1);
148
+ handlers = handlers.slice();
149
+ for (i = 0; i < handlers.length; i++) {
150
+ try {
151
+ handlers[i].apply(null, args);
152
+ } catch (error) {
153
+ if (typeof console !== 'undefined' && console.error) {
154
+ console.error('[EventEmitter] "' + eventName + '" handler error:', error);
155
+ }
156
+ }
157
+ }
158
+ return this;
159
+ };
160
+
161
+ var DEFAULT_SOURCE = 'KADOU_GAME_SDK';
162
+ function PostMessageChannel(options) {
163
+ options = options || {};
164
+ this.selfWindow = options.selfWindow || window;
165
+ this.targetWindow = options.targetWindow || null;
166
+ this.targetOrigin = options.targetOrigin || '*';
167
+ this.source = options.source || DEFAULT_SOURCE;
168
+ this.debug = !!options.debug;
169
+ }
170
+ PostMessageChannel.prototype.setTargetWindow = function (targetWindow) {
171
+ this.targetWindow = targetWindow;
172
+ return this;
173
+ };
174
+ PostMessageChannel.prototype.setTargetOrigin = function (targetOrigin) {
175
+ this.targetOrigin = targetOrigin || '*';
176
+ return this;
177
+ };
178
+ PostMessageChannel.prototype.createMessage = function (type, payload, meta) {
179
+ payload = payload || {};
180
+ meta = meta || {};
181
+ return {
182
+ source: this.source,
183
+ version: SDK_VERSION,
184
+ type: type,
185
+ requestId: createRequestId(),
186
+ timestamp: new Date().getTime(),
187
+ payload: isObject(payload) ? payload : {
188
+ value: payload
189
+ },
190
+ meta: isObject(meta) ? meta : {}
191
+ };
192
+ };
193
+ PostMessageChannel.prototype.send = function (type, payload, meta) {
194
+ var message;
195
+ if (!this.targetWindow || !this.targetWindow.postMessage) {
196
+ if (this.debug && typeof console !== 'undefined' && console.warn) {
197
+ console.warn('[PostMessageChannel] targetWindow 不存在,消息发送失败:', type);
198
+ }
199
+ return null;
200
+ }
201
+ message = this.createMessage(type, payload, meta);
202
+ this.targetWindow.postMessage(message, this.targetOrigin);
203
+ if (this.debug && typeof console !== 'undefined' && console.log) {
204
+ console.log('[PostMessageChannel] send =>', message);
205
+ }
206
+ return message;
207
+ };
208
+ PostMessageChannel.prototype.isValidMessage = function (data) {
209
+ return !!(data && typeof data === 'object' && data.source === this.source && typeof data.type === 'string');
210
+ };
211
+
212
+ function MessageBus(options) {
213
+ options = options || {};
214
+ EventEmitter.call(this);
215
+ this.channel = new PostMessageChannel(options);
216
+ this.selfWindow = options.selfWindow || window;
217
+ this.allowedOrigins = options.allowedOrigins || ['*'];
218
+ this.debug = !!options.debug;
219
+ this._boundOnMessage = this._onMessage.bind(this);
220
+ this._listening = false;
221
+ }
222
+ MessageBus.prototype = Object.create(EventEmitter.prototype);
223
+ MessageBus.prototype.constructor = MessageBus;
224
+ MessageBus.prototype.start = function () {
225
+ if (this._listening) return this;
226
+ this.selfWindow.addEventListener('message', this._boundOnMessage, false);
227
+ this._listening = true;
228
+ if (this.debug && typeof console !== 'undefined' && console.log) {
229
+ console.log('[MessageBus] started');
230
+ }
231
+ return this;
232
+ };
233
+ MessageBus.prototype.stop = function () {
234
+ if (!this._listening) return this;
235
+ this.selfWindow.removeEventListener('message', this._boundOnMessage, false);
236
+ this._listening = false;
237
+ if (this.debug && typeof console !== 'undefined' && console.log) {
238
+ console.log('[MessageBus] stopped');
239
+ }
240
+ return this;
241
+ };
242
+ MessageBus.prototype.destroy = function () {
243
+ this.stop();
244
+ this.off();
245
+ return this;
246
+ };
247
+ MessageBus.prototype.setTargetWindow = function (targetWindow) {
248
+ this.channel.setTargetWindow(targetWindow);
249
+ return this;
250
+ };
251
+ MessageBus.prototype.setTargetOrigin = function (targetOrigin) {
252
+ this.channel.setTargetOrigin(targetOrigin);
253
+ return this;
254
+ };
255
+ MessageBus.prototype.send = function (type, payload, meta) {
256
+ return this.channel.send(type, payload, meta);
257
+ };
258
+ MessageBus.prototype._matchOrigin = function (origin) {
259
+ var i;
260
+ if (!this.allowedOrigins || !this.allowedOrigins.length) return true;
261
+ if (this.allowedOrigins[0] === '*' || this.allowedOrigins.indexOf('*') > -1) return true;
262
+ for (i = 0; i < this.allowedOrigins.length; i++) {
263
+ if (this.allowedOrigins[i] === origin) {
264
+ return true;
265
+ }
266
+ }
267
+ return false;
268
+ };
269
+ MessageBus.prototype._onMessage = function (event) {
270
+ var data = event.data;
271
+ var origin = event.origin;
272
+ var source = event.source;
273
+ var context;
274
+ if (!this._matchOrigin(origin)) {
275
+ if (this.debug && typeof console !== 'undefined' && console.warn) {
276
+ console.warn('[MessageBus] origin 不匹配,已忽略:', origin);
277
+ }
278
+ return;
279
+ }
280
+ if (!this.channel.isValidMessage(data)) return;
281
+ if (this.debug && typeof console !== 'undefined' && console.log) {
282
+ console.log('[MessageBus] receive <=', data);
283
+ }
284
+ context = {
285
+ rawEvent: event,
286
+ origin: origin,
287
+ sourceWindow: source
288
+ };
289
+ this.emit('*', data, context);
290
+ this.emit(data.type, data.payload, data, context);
291
+ };
292
+
293
+ function BasePage() {
294
+ this.el = null;
295
+ this.events = [];
296
+ }
297
+
298
+ /**
299
+ * 绑定事件,并记录下来,destroy 时统一卸载
300
+ */
301
+ BasePage.prototype.bindEvent = function (el, type, handler, options) {
302
+ if (!el || !type || !handler) return;
303
+ el.addEventListener(type, handler, options || false);
304
+ this.events.push({
305
+ el: el,
306
+ type: type,
307
+ handler: handler,
308
+ options: options || false
309
+ });
310
+ };
311
+
312
+ /**
313
+ * 解绑全部事件
314
+ */
315
+ BasePage.prototype.unbindEvents = function () {
316
+ var i;
317
+ var item;
318
+ for (i = 0; i < this.events.length; i++) {
319
+ item = this.events[i];
320
+ if (item && item.el) {
321
+ item.el.removeEventListener(item.type, item.handler, item.options);
322
+ }
323
+ }
324
+ this.events = [];
325
+ };
326
+
327
+ /**
328
+ * 销毁页面
329
+ */
330
+ BasePage.prototype.destroy = function () {
331
+ this.unbindEvents();
332
+ if (this.el && this.el.parentNode) {
333
+ this.el.parentNode.removeChild(this.el);
334
+ }
335
+ this.el = null;
336
+ };
337
+
338
+ /**
339
+ * 是否已渲染
340
+ */
341
+ BasePage.prototype.isRendered = function () {
342
+ return !!this.el;
343
+ };
344
+
345
+ function isPlainObject(obj) {
346
+ return Object.prototype.toString.call(obj) === '[object Object]';
347
+ }
348
+ function mergeStyles(base, extra) {
349
+ var result = {};
350
+ var key;
351
+ base = isPlainObject(base) ? base : {};
352
+ extra = isPlainObject(extra) ? extra : {};
353
+ for (key in base) {
354
+ if (Object.prototype.hasOwnProperty.call(base, key)) {
355
+ result[key] = base[key];
356
+ }
357
+ }
358
+ for (key in extra) {
359
+ if (Object.prototype.hasOwnProperty.call(extra, key)) {
360
+ result[key] = extra[key];
361
+ }
362
+ }
363
+ return result;
364
+ }
365
+ function normalizeStyleValue(key, value) {
366
+ if (value == null) return '';
367
+ if (key === 'backgroundImage' && value) {
368
+ if (typeof value === 'string' && value.indexOf('url(') !== 0 && value !== 'none') {
369
+ return 'url("' + value + '")';
370
+ }
371
+ }
372
+ return String(value);
373
+ }
374
+ function applyStyles(el, styles) {
375
+ var key;
376
+ if (!el || !styles) return;
377
+ for (key in styles) {
378
+ if (Object.prototype.hasOwnProperty.call(styles, key)) {
379
+ el.style[key] = normalizeStyleValue(key, styles[key]);
380
+ }
381
+ }
382
+ }
383
+
384
+ function createElement(tag, className, styles, text) {
385
+ var el = document.createElement(tag);
386
+ if (className) {
387
+ el.className = className;
388
+ }
389
+ if (styles) {
390
+ applyStyles(el, styles);
391
+ }
392
+ if (text != null) {
393
+ el.textContent = String(text);
394
+ }
395
+ return el;
396
+ }
397
+
398
+ function StartPage() {
399
+ BasePage.call(this);
400
+ }
401
+ StartPage.prototype = Object.create(BasePage.prototype);
402
+ StartPage.prototype.constructor = StartPage;
403
+
404
+ /**
405
+ * 获取最终样式
406
+ * 不再自己根据 themeKey 查主题
407
+ * 直接使用 game-manager 传进来的最终页面配置
408
+ */
409
+ StartPage.prototype.getStyles = function (data) {
410
+ data = data || {};
411
+ var wrapperCss = mergeStyles({
412
+ position: 'absolute',
413
+ top: '0',
414
+ left: '0',
415
+ right: '0',
416
+ bottom: '0',
417
+ display: 'flex',
418
+ flexDirection: 'column',
419
+ alignItems: 'center',
420
+ justifyContent: 'center',
421
+ boxSizing: 'border-box',
422
+ padding: '40px',
423
+ color: '#ffffff',
424
+ backgroundRepeat: 'no-repeat',
425
+ backgroundPosition: 'center center',
426
+ backgroundSize: 'cover',
427
+ textAlign: 'center',
428
+ zIndex: '99999'
429
+ }, data.wrapper);
430
+ var overlayCss = mergeStyles({
431
+ position: 'absolute',
432
+ top: '0',
433
+ left: '0',
434
+ right: '0',
435
+ bottom: '0',
436
+ background: 'rgba(0, 0, 0, 0.28)'
437
+ }, data.overlay);
438
+ var contentCss = mergeStyles({
439
+ position: 'relative',
440
+ zIndex: '1',
441
+ width: '100%',
442
+ display: 'flex',
443
+ flexDirection: 'column',
444
+ alignItems: 'center'
445
+ }, data.content);
446
+ var titleCss = mergeStyles({
447
+ fontSize: '85px',
448
+ fontWeight: 'bold',
449
+ textShadow: '2px 2px 4px #0b1c32',
450
+ letterSpacing: '5px',
451
+ lineHeight: '1.3',
452
+ maxWidth: '90%',
453
+ wordBreak: 'break-word'
454
+ }, data.title);
455
+ var descCss = mergeStyles({
456
+ marginTop: '30px',
457
+ width: '100%',
458
+ whiteSpace: 'pre-wrap',
459
+ lineHeight: '1.6',
460
+ wordBreak: 'break-word',
461
+ fontSize: '40px'
462
+ }, data.desc);
463
+ var btnGroupCss = mergeStyles({
464
+ width: '100%',
465
+ maxWidth: '800px',
466
+ marginTop: '80px',
467
+ display: 'flex',
468
+ justifyContent: 'center',
469
+ alignItems: 'center'
470
+ }, data.buttonGroup);
471
+ var btnCss = mergeStyles({
472
+ display: 'inline-flex',
473
+ alignItems: 'center',
474
+ justifyContent: 'center',
475
+ minWidth: '240px',
476
+ minHeight: '88px',
477
+ padding: '20px 50px',
478
+ boxSizing: 'border-box',
479
+ cursor: 'pointer',
480
+ fontSize: '40px',
481
+ fontWeight: 'bold',
482
+ backgroundColor: '#04b6bb',
483
+ backgroundRepeat: 'no-repeat',
484
+ backgroundPosition: 'center center',
485
+ backgroundSize: 'cover',
486
+ borderRadius: '50px',
487
+ border: 'none',
488
+ color: '#fff',
489
+ transition: 'all 0.2s linear'
490
+ }, data.button);
491
+ return {
492
+ wrapperCss: wrapperCss,
493
+ overlayCss: overlayCss,
494
+ contentCss: contentCss,
495
+ titleCss: titleCss,
496
+ descCss: descCss,
497
+ btnGroupCss: btnGroupCss,
498
+ btnCss: btnCss
499
+ };
500
+ };
501
+ StartPage.prototype.render = function (container, data, onStart) {
502
+ data = data || {};
503
+ if (!container) {
504
+ throw new Error('[StartPage] render 失败:container 不存在');
505
+ }
506
+ this.destroy();
507
+ var styles = this.getStyles(data);
508
+ var titleText = data.titleText || '欢迎进入游戏';
509
+ var descText = typeof data.descText === 'string' ? data.descText : '';
510
+ var buttonText = data.buttonText || '开始游戏';
511
+ var rootDom = createElement('div', 'kadou-start-page', styles.wrapperCss);
512
+ var overlayDom = createElement('div', 'kadou-start-overlay', styles.overlayCss);
513
+ var contentDom = createElement('div', 'kadou-start-content', styles.contentCss);
514
+ var titleDom = createElement('div', 'kadou-start-title', styles.titleCss, titleText);
515
+ rootDom.appendChild(overlayDom);
516
+ contentDom.appendChild(titleDom);
517
+ if (descText) {
518
+ var descDom = createElement('div', 'kadou-start-desc', styles.descCss, descText);
519
+ contentDom.appendChild(descDom);
520
+ }
521
+ var btnGroupDom = createElement('div', 'kadou-start-btn-group', styles.btnGroupCss);
522
+ var btnDom = createElement('div', 'kadou-start-btn', styles.btnCss, buttonText);
523
+ this.bindEvent(btnDom, 'click', function () {
524
+ if (typeof onStart === 'function') {
525
+ onStart();
526
+ }
527
+ });
528
+ btnGroupDom.appendChild(btnDom);
529
+ contentDom.appendChild(btnGroupDom);
530
+ rootDom.appendChild(contentDom);
531
+ container.appendChild(rootDom);
532
+ this.el = rootDom;
533
+ return rootDom;
534
+ };
535
+
536
+ function EndPage() {
537
+ BasePage.call(this);
538
+ }
539
+ EndPage.prototype = Object.create(BasePage.prototype);
540
+ EndPage.prototype.constructor = EndPage;
541
+
542
+ /**
543
+ * 获取最终样式
544
+ * 直接使用 game-manager 传进来的最终页面配置
545
+ */
546
+ EndPage.prototype.getStyles = function (data) {
547
+ data = data || {};
548
+ var wrapperCss = mergeStyles({
549
+ position: 'absolute',
550
+ top: '0',
551
+ left: '0',
552
+ right: '0',
553
+ bottom: '0',
554
+ display: 'flex',
555
+ flexDirection: 'column',
556
+ alignItems: 'center',
557
+ justifyContent: 'center',
558
+ boxSizing: 'border-box',
559
+ padding: '40px',
560
+ color: '#ffffff',
561
+ backgroundRepeat: 'no-repeat',
562
+ backgroundPosition: 'center center',
563
+ backgroundSize: 'cover',
564
+ textAlign: 'center',
565
+ zIndex: '99999'
566
+ }, data.wrapper);
567
+ var overlayCss = mergeStyles({
568
+ position: 'absolute',
569
+ top: '0',
570
+ left: '0',
571
+ right: '0',
572
+ bottom: '0',
573
+ background: 'rgba(0, 0, 0, 0.35)'
574
+ }, data.overlay);
575
+ var contentCss = mergeStyles({
576
+ position: 'relative',
577
+ zIndex: '1',
578
+ width: '100%',
579
+ display: 'flex',
580
+ flexDirection: 'column',
581
+ alignItems: 'center'
582
+ }, data.content);
583
+ var titleCss = mergeStyles({
584
+ fontSize: '72px',
585
+ fontWeight: 'bold',
586
+ lineHeight: '1.3',
587
+ maxWidth: '90%',
588
+ wordBreak: 'break-word',
589
+ textShadow: '2px 2px 4px #0b1c32'
590
+ }, data.title);
591
+ var timeCss = mergeStyles({
592
+ marginTop: '28px',
593
+ fontSize: '40px',
594
+ wordBreak: 'break-word'
595
+ }, data.time);
596
+ var scoreCss = mergeStyles({
597
+ marginTop: '28px',
598
+ fontSize: '40px',
599
+ wordBreak: 'break-word'
600
+ }, data.score);
601
+ var btnGroupCss = mergeStyles({
602
+ width: '100%',
603
+ maxWidth: '900px',
604
+ marginTop: '70px',
605
+ display: 'flex',
606
+ justifyContent: 'center',
607
+ alignItems: 'center',
608
+ gap: '24px',
609
+ flexWrap: 'wrap'
610
+ }, data.buttonGroup);
611
+ var primaryBtnCss = mergeStyles({
612
+ display: 'inline-flex',
613
+ alignItems: 'center',
614
+ justifyContent: 'center',
615
+ padding: '20px 50px',
616
+ boxSizing: 'border-box',
617
+ cursor: 'pointer',
618
+ fontSize: '40px',
619
+ fontWeight: 'bold',
620
+ background: '#04b6bb',
621
+ borderRadius: '50px',
622
+ border: 'none',
623
+ color: '#fff',
624
+ transition: 'all 0.2s linear'
625
+ }, data.primaryButton || data.button);
626
+ var secondaryBtnCss = mergeStyles({
627
+ display: 'inline-flex',
628
+ alignItems: 'center',
629
+ justifyContent: 'center',
630
+ padding: '20px 50px',
631
+ boxSizing: 'border-box',
632
+ cursor: 'pointer',
633
+ fontSize: '40px',
634
+ fontWeight: 'bold',
635
+ background: '#ff6600',
636
+ borderRadius: '50px',
637
+ color: '#fff',
638
+ transition: 'all 0.2s linear'
639
+ }, data.secondaryButton);
640
+ return {
641
+ wrapperCss: wrapperCss,
642
+ overlayCss: overlayCss,
643
+ contentCss: contentCss,
644
+ titleCss: titleCss,
645
+ timeCss: timeCss,
646
+ scoreCss: scoreCss,
647
+ btnGroupCss: btnGroupCss,
648
+ primaryBtnCss: primaryBtnCss,
649
+ secondaryBtnCss: secondaryBtnCss
650
+ };
651
+ };
652
+
653
+ /**
654
+ * 渲染结束页
655
+ * callbacks:
656
+ * - onRestart
657
+ * - onClose
658
+ */
659
+ EndPage.prototype.render = function (container, data, callbacks) {
660
+ data = data || {};
661
+ callbacks = callbacks || {};
662
+ if (!container) {
663
+ throw new Error('[EndPage] render 失败:container 不存在');
664
+ }
665
+ this.destroy();
666
+ var styles = this.getStyles(data);
667
+ var titleText = data.titleText || '游戏结束';
668
+ var timeText = typeof data.timeText === 'string' ? data.timeText : '';
669
+ var scoreText = typeof data.scoreText === 'string' ? data.scoreText : '';
670
+ var primaryButtonText = data.primaryButtonText || data.buttonText || '再来一次';
671
+ var secondaryButtonText = data.secondaryButtonText || '';
672
+ var rootDom = createElement('div', 'kadou-end-page', styles.wrapperCss);
673
+ var overlayDom = createElement('div', 'kadou-end-overlay', styles.overlayCss);
674
+ var contentDom = createElement('div', 'kadou-end-content', styles.contentCss);
675
+ var titleDom = createElement('div', 'kadou-end-title', styles.titleCss, titleText);
676
+ rootDom.appendChild(overlayDom);
677
+ contentDom.appendChild(titleDom);
678
+ if (scoreText) {
679
+ var scoreDom = createElement('div', 'kadou-end-score', styles.scoreCss, scoreText);
680
+ contentDom.appendChild(scoreDom);
681
+ }
682
+ if (timeText) {
683
+ var timeDom = createElement('div', 'kadou-end-time', styles.timeCss, timeText);
684
+ contentDom.appendChild(timeDom);
685
+ }
686
+ var btnGroupDom = createElement('div', 'kadou-end-btn-group', styles.btnGroupCss);
687
+ if (secondaryButtonText) {
688
+ var secondaryBtnDom = createElement('div', 'kadou-end-secondary-btn', styles.secondaryBtnCss, secondaryButtonText);
689
+ this.bindEvent(secondaryBtnDom, 'click', function () {
690
+ if (typeof callbacks.onShowRank === 'function') {
691
+ callbacks.onShowRank();
692
+ }
693
+ });
694
+ btnGroupDom.appendChild(secondaryBtnDom);
695
+ }
696
+ {
697
+ var primaryBtnDom = createElement('div', 'kadou-end-primary-btn', styles.primaryBtnCss, primaryButtonText);
698
+ this.bindEvent(primaryBtnDom, 'click', function () {
699
+ if (typeof callbacks.onRestart === 'function') {
700
+ callbacks.onRestart();
701
+ }
702
+ });
703
+ btnGroupDom.appendChild(primaryBtnDom);
704
+ }
705
+ if (btnGroupDom.childNodes.length > 0) {
706
+ contentDom.appendChild(btnGroupDom);
707
+ }
708
+ rootDom.appendChild(contentDom);
709
+ container.appendChild(rootDom);
710
+ this.el = rootDom;
711
+ return rootDom;
712
+ };
713
+
714
+ function OverlayManager(options) {
715
+ options = options || {};
716
+ this.container = options.container || document.body;
717
+ this.startPage = null;
718
+ this.endPage = null;
719
+ }
720
+
721
+ /**
722
+ * 设置容器
723
+ */
724
+ OverlayManager.prototype.setContainer = function (container) {
725
+ if (container) {
726
+ this.container = container;
727
+ }
728
+ return this;
729
+ };
730
+
731
+ /**
732
+ * 显示开始页
733
+ * data 直接使用统一 pageData 结构
734
+ */
735
+ OverlayManager.prototype.showStartPage = function (data, callbacks) {
736
+ var self = this;
737
+ var finalCallbacks = callbacks || {};
738
+ if (!this.container) {
739
+ throw new Error('[OverlayManager] showStartPage 失败:container 不存在');
740
+ }
741
+ this.hideStartPage();
742
+ this.startPage = new StartPage();
743
+ this.startPage.render(this.container, data || {}, function () {
744
+ if (typeof finalCallbacks.onStart === 'function') {
745
+ finalCallbacks.onStart();
746
+ }
747
+
748
+ // 如果没传回调,也不自动销毁,由外部决定何时关闭
749
+ // 想点按钮立刻隐藏,可以把下面这行打开
750
+ self.hideStartPage();
751
+ });
752
+ return self;
753
+ };
754
+
755
+ /**
756
+ * 隐藏开始页
757
+ */
758
+ OverlayManager.prototype.hideStartPage = function () {
759
+ if (this.startPage) {
760
+ this.startPage.destroy();
761
+ this.startPage = null;
762
+ }
763
+ return this;
764
+ };
765
+
766
+ /**
767
+ * 显示结束页
768
+ */
769
+ OverlayManager.prototype.showEndPage = function (data, callbacks) {
770
+ var self = this;
771
+ var finalCallbacks = callbacks || {};
772
+ if (!this.container) {
773
+ throw new Error('[OverlayManager] showEndPage 失败:container 不存在');
774
+ }
775
+ this.hideEndPage();
776
+ this.endPage = new EndPage();
777
+ this.endPage.render(this.container, data || {}, {
778
+ onRestart: function onRestart() {
779
+ if (typeof finalCallbacks.onRestart === 'function') {
780
+ finalCallbacks.onRestart();
781
+ self.hideEndPage();
782
+ }
783
+ },
784
+ onShowRank: function onShowRank() {
785
+ if (typeof finalCallbacks.onShowRank === 'function') {
786
+ finalCallbacks.onShowRank();
787
+ }
788
+ }
789
+ });
790
+ return self;
791
+ };
792
+
793
+ /**
794
+ * 隐藏结束页
795
+ */
796
+ OverlayManager.prototype.hideEndPage = function () {
797
+ if (this.endPage) {
798
+ this.endPage.destroy();
799
+ this.endPage = null;
800
+ }
801
+ return this;
802
+ };
803
+
804
+ /**
805
+ * 全部隐藏
806
+ */
807
+ OverlayManager.prototype.hideAll = function () {
808
+ this.hideStartPage();
809
+ this.hideEndPage();
810
+ return this;
811
+ };
812
+
813
+ /**
814
+ * 销毁管理器
815
+ */
816
+ OverlayManager.prototype.destroy = function () {
817
+ this.hideAll();
818
+ this.container = null;
819
+ };
820
+
821
+ var themes = {};
822
+
823
+ /**
824
+ * 注册主题
825
+ * @param {string} key 主题 key
826
+ * @param {object} config 主题配置
827
+ */
828
+ function registerTheme(key, config) {
829
+ if (!key) {
830
+ throw new Error('Theme key is required');
831
+ }
832
+ themes[key] = config || {};
833
+ }
834
+
835
+ /**
836
+ * 是否已存在主题
837
+ * @param {string} key
838
+ * @returns {boolean}
839
+ */
840
+ function hasTheme(key) {
841
+ return !!themes[key];
842
+ }
843
+
844
+ /**
845
+ * 获取主题
846
+ * @param {string} key
847
+ * @returns {object|null}
848
+ */
849
+ function getTheme(key) {
850
+ return themes[key] || themes.default || null;
851
+ }
852
+
853
+ function dirname(url) {
854
+ if (!url) return '';
855
+ return url.replace(/\/[^\/]*$/, '/');
856
+ }
857
+ function isAbsoluteUrl(url) {
858
+ return /^(https?:)?\/\//.test(url) || /^file:/.test(url) || /^data:/.test(url) || /^blob:/.test(url);
859
+ }
860
+ function joinUrl(base, path) {
861
+ if (!path) return '';
862
+ if (isAbsoluteUrl(path)) return path;
863
+ return base + path.replace(/^\.\//, '');
864
+ }
865
+ function patchStyleAssetPaths(styleObj, baseUrl) {
866
+ var key;
867
+ var value;
868
+ if (!styleObj) return styleObj;
869
+ for (key in styleObj) {
870
+ if (!Object.prototype.hasOwnProperty.call(styleObj, key)) continue;
871
+ value = styleObj[key];
872
+ if (typeof value !== 'string') continue;
873
+ if (key === 'backgroundImage') {
874
+ styleObj[key] = joinUrl(baseUrl, value);
875
+ continue;
876
+ }
877
+ if (key === 'src') {
878
+ styleObj[key] = joinUrl(baseUrl, value);
879
+ continue;
880
+ }
881
+ if (key === 'background') {
882
+ styleObj[key] = value.replace(/url\((['"]?)(.+?)\1\)/g, function (_, quote, assetPath) {
883
+ return 'url("' + joinUrl(baseUrl, assetPath) + '")';
884
+ });
885
+ }
886
+ }
887
+ return styleObj;
888
+ }
889
+ function patchHostAssetPaths(hostConfig, baseUrl) {
890
+ var keys;
891
+ var i;
892
+ var key;
893
+ var value;
894
+ if (!hostConfig) return hostConfig;
895
+ keys = ['bg', 'frame', 'actionBtn', 'logo', 'icon'];
896
+ for (i = 0; i < keys.length; i++) {
897
+ key = keys[i];
898
+ value = hostConfig[key];
899
+ if (typeof value === 'string' && value) {
900
+ hostConfig[key] = joinUrl(baseUrl, value);
901
+ }
902
+ }
903
+ return hostConfig;
904
+ }
905
+ function patchThemeAssetPaths(theme, baseUrl) {
906
+ var startPage;
907
+ var endPage;
908
+ var pageKeys;
909
+ var i;
910
+ if (!theme) return theme;
911
+ startPage = theme.startPage || {};
912
+ endPage = theme.endPage || {};
913
+ pageKeys = ['wrapper', 'overlay', 'content', 'title', 'desc', 'buttonGroup', 'button', 'primaryButton', 'secondaryButton', 'score'];
914
+ for (i = 0; i < pageKeys.length; i++) {
915
+ patchStyleAssetPaths(startPage[pageKeys[i]], baseUrl);
916
+ patchStyleAssetPaths(endPage[pageKeys[i]], baseUrl);
917
+ }
918
+ patchHostAssetPaths(theme.host, baseUrl);
919
+ return theme;
920
+ }
921
+ function loadScript(url, callback) {
922
+ var script = document.createElement('script');
923
+ var done = false;
924
+ script.type = 'text/javascript';
925
+ script.async = true;
926
+ script.src = url;
927
+ script.onload = function () {
928
+ if (done) return;
929
+ done = true;
930
+ callback && callback(null, script);
931
+ };
932
+ script.onerror = function () {
933
+ if (done) return;
934
+ done = true;
935
+ callback && callback(new Error('Failed to load script: ' + url));
936
+ };
937
+ document.head.appendChild(script);
938
+ }
939
+ function loadThemeByUrl(url, callback) {
940
+ window.__KADOU_THEME_PACK__ = null;
941
+ loadScript(url, function (err) {
942
+ var pack;
943
+ var baseUrl;
944
+ var theme;
945
+ if (err) {
946
+ callback && callback(err);
947
+ return;
948
+ }
949
+ pack = window.__KADOU_THEME_PACK__;
950
+ if (!pack || !pack.key || !pack.theme) {
951
+ callback && callback(new Error('Invalid theme pack: ' + url));
952
+ return;
953
+ }
954
+ baseUrl = dirname(url);
955
+ theme = patchThemeAssetPaths(pack.theme, baseUrl);
956
+ registerTheme(pack.key, theme);
957
+ callback && callback(null, {
958
+ key: pack.key,
959
+ url: url,
960
+ from: 'remote',
961
+ theme: theme
962
+ });
963
+ });
964
+ }
965
+ function buildThemeUrl(themeRoot, themeKey) {
966
+ if (!themeRoot) {
967
+ throw new Error('themeRoot is required');
968
+ }
969
+ if (!themeKey) {
970
+ throw new Error('themeKey is required');
971
+ }
972
+ return themeRoot.replace(/\/?$/, '/') + themeKey + '/theme.js';
973
+ }
974
+ function ensureTheme(themeKey, themeRoot, callback) {
975
+ if (hasTheme(themeKey)) {
976
+ callback && callback(null, {
977
+ key: themeKey,
978
+ from: 'cache',
979
+ theme: getTheme(themeKey)
980
+ });
981
+ return;
982
+ }
983
+ var url;
984
+ try {
985
+ url = buildThemeUrl(themeRoot, themeKey);
986
+ } catch (err) {
987
+ callback && callback(err);
988
+ return;
989
+ }
990
+ loadThemeByUrl(url, callback);
991
+ }
992
+
993
+ /**
994
+ * GameManager
995
+ * -----------------------------------------
996
+ * 游戏管理器
997
+ *
998
+ * 作用:
999
+ * 1 管理游戏生命周期
1000
+ * 2 统一发送 / 接收 postMessage
1001
+ * 3 提供事件机制给外部使用
1002
+ * 4 管理 SDK 与 Game 的状态
1003
+ * 5 host 负责主题加载,game 负责主题渲染
1004
+ *
1005
+ * 架构关系:
1006
+ *
1007
+ * Host 页面 (Vue 壳)
1008
+ * │
1009
+ * │ postMessage
1010
+ * ▼
1011
+ * GameManager <----> MessageBus
1012
+ * │
1013
+ * ▼
1014
+ * Game iframe
1015
+ *
1016
+ */
1017
+
1018
+
1019
+ /**
1020
+ * 构造函数
1021
+ */
1022
+ function GameManager(options) {
1023
+ options = options || {};
1024
+
1025
+ // 继承事件系统
1026
+ EventEmitter.call(this);
1027
+ this.options = options;
1028
+
1029
+ /**
1030
+ * UI 挂载根节点
1031
+ */
1032
+ this.root = options.root || document.body;
1033
+
1034
+ /**
1035
+ * 是否开启调试
1036
+ */
1037
+ this.debug = !!options.debug;
1038
+
1039
+ /**
1040
+ * 角色
1041
+ * host = 宿主页面
1042
+ * game = 游戏页面
1043
+ */
1044
+ this.role = options.role || 'host';
1045
+
1046
+ /**
1047
+ * SDK 状态
1048
+ */
1049
+ this.state = {
1050
+ sdkReady: false,
1051
+ gameReady: false,
1052
+ started: false,
1053
+ paused: false,
1054
+ destroyed: false
1055
+ };
1056
+
1057
+ /**
1058
+ * 当前主题
1059
+ * 仅用于 game 内部渲染
1060
+ */
1061
+ this.currentThemeKey = '';
1062
+ this.currentTheme = null;
1063
+
1064
+ /**
1065
+ * MessageBus
1066
+ * 负责 postMessage 封装
1067
+ */
1068
+ this.bus = new MessageBus({
1069
+ selfWindow: options.selfWindow || window,
1070
+ targetWindow: options.targetWindow || null,
1071
+ targetOrigin: options.targetOrigin || '*',
1072
+ allowedOrigins: options.allowedOrigins || ['*'],
1073
+ source: options.source || 'KADOU_GAME_SDK',
1074
+ debug: this.debug
1075
+ });
1076
+
1077
+ /**
1078
+ * UI Overlay 管理器
1079
+ * 只有 game 角色才负责真正渲染开始页 / 结束页
1080
+ */
1081
+ this.overlay = this.role === 'game' ? new OverlayManager({
1082
+ container: this.root
1083
+ }) : null;
1084
+
1085
+ /**
1086
+ * 绑定 MessageBus 事件
1087
+ */
1088
+ this._bindBusEvents();
1089
+ }
1090
+ GameManager.prototype = Object.create(EventEmitter.prototype);
1091
+ GameManager.prototype.constructor = GameManager;
1092
+
1093
+ /**
1094
+ * 绑定 MessageBus 所有事件
1095
+ */
1096
+ GameManager.prototype._bindBusEvents = function () {
1097
+ var self = this;
1098
+
1099
+ /**
1100
+ * 所有消息
1101
+ */
1102
+ this.bus.on('*', function (message, context) {
1103
+ self.emit('message', message, context);
1104
+ });
1105
+
1106
+ /**
1107
+ * SDK READY
1108
+ */
1109
+ this.bus.on(MESSAGE_TYPES.SDK_READY, function (payload, message, context) {
1110
+ self.state.sdkReady = true;
1111
+ self.emit('sdk:ready', payload, message, context);
1112
+ });
1113
+
1114
+ /**
1115
+ * GAME READY
1116
+ */
1117
+ this.bus.on(MESSAGE_TYPES.GAME_READY, function (payload, message, context) {
1118
+ self.state.gameReady = true;
1119
+ self.emit('game:ready', payload, message, context);
1120
+ });
1121
+
1122
+ /**
1123
+ * GAME LOAD
1124
+ */
1125
+ this.bus.on(MESSAGE_TYPES.GAME_LOAD, function (payload, message, context) {
1126
+ self.emit('game:load', payload, message, context);
1127
+ });
1128
+
1129
+ /**
1130
+ * GAME LOADED
1131
+ */
1132
+ this.bus.on(MESSAGE_TYPES.GAME_LOADED, function (payload, message, context) {
1133
+ self.emit('game:loaded', payload, message, context);
1134
+ });
1135
+
1136
+ /**
1137
+ * GAME LOADING
1138
+ */
1139
+ this.bus.on(MESSAGE_TYPES.GAME_LOADING, function (payload, message, context) {
1140
+ self.emit('game:loading', payload, message, context);
1141
+ });
1142
+
1143
+ /**
1144
+ * GAME START
1145
+ */
1146
+ this.bus.on(MESSAGE_TYPES.GAME_START, function (payload, message, context) {
1147
+ self.state.started = true;
1148
+ self.state.paused = false;
1149
+ if (self.role === 'game' && self.overlay) {
1150
+ self.hideStartPage();
1151
+ }
1152
+ self.emit('game:start', payload, message, context);
1153
+ });
1154
+
1155
+ /**
1156
+ * GAME START
1157
+ */
1158
+ this.bus.on(MESSAGE_TYPES.GAME_RESTART, function (payload, message, context) {
1159
+ self.state.started = true;
1160
+ self.state.paused = false;
1161
+ if (self.role === 'game' && self.overlay) {
1162
+ self.hideEndPage();
1163
+ }
1164
+ self.emit('game:restart', payload, message, context);
1165
+ });
1166
+
1167
+ /**
1168
+ * GAME PAUSE
1169
+ */
1170
+ this.bus.on(MESSAGE_TYPES.GAME_PAUSE, function (payload, message, context) {
1171
+ self.state.paused = true;
1172
+ self.emit('game:pause', payload, message, context);
1173
+ });
1174
+
1175
+ /**
1176
+ * GAME RESUME
1177
+ */
1178
+ this.bus.on(MESSAGE_TYPES.GAME_RESUME, function (payload, message, context) {
1179
+ self.state.paused = false;
1180
+ self.emit('game:resume', payload, message, context);
1181
+ });
1182
+
1183
+ /**
1184
+ * GAME FINISH
1185
+ */
1186
+ this.bus.on(MESSAGE_TYPES.GAME_FINISH, function (payload, message, context) {
1187
+ self.state.started = false;
1188
+ self.emit('game:finish', payload, message, context);
1189
+ });
1190
+
1191
+ /**
1192
+ * GAME DESTROY
1193
+ */
1194
+ this.bus.on(MESSAGE_TYPES.GAME_DESTROY, function (payload, message, context) {
1195
+ self.state.destroyed = true;
1196
+ self.emit('game:destroy', payload, message, context);
1197
+ });
1198
+
1199
+ /**
1200
+ * 主题更新
1201
+ * 只缓存,不直接渲染
1202
+ */
1203
+ this.bus.on(MESSAGE_TYPES.THEME_UPDATE, function (payload, message, context) {
1204
+ self.currentThemeKey = payload && payload.themeKey ? payload.themeKey : '';
1205
+ self.currentTheme = payload && payload.theme ? payload.theme : payload || null;
1206
+ self.emit('theme:update', payload, message, context);
1207
+ });
1208
+
1209
+ /**
1210
+ * 配置更新
1211
+ */
1212
+ this.bus.on(MESSAGE_TYPES.CONFIG_UPDATE, function (payload, message, context) {
1213
+ self.emit('config:update', payload, message, context);
1214
+ });
1215
+
1216
+ /**
1217
+ * 学生提交答案
1218
+ */
1219
+ this.bus.on(MESSAGE_TYPES.QUESTION_SUBMIT, function (payload, message, context) {
1220
+ self.emit('question:submit', payload, message, context);
1221
+ });
1222
+
1223
+ /**
1224
+ * 游戏结果上报
1225
+ */
1226
+ this.bus.on(MESSAGE_TYPES.REPORT_GENERATED, function (payload, message, context) {
1227
+ self.emit('result:report', payload, message, context);
1228
+ });
1229
+
1230
+ /**
1231
+ * 显示开始页
1232
+ * host 发消息,game 内部真正 render
1233
+ */
1234
+ this.bus.on(MESSAGE_TYPES.PAGE_START_SHOW, function (payload, message, context) {
1235
+ self.emit('page:start:show', payload, message, context);
1236
+ if (self.role === 'game') {
1237
+ self.renderStartPage(payload);
1238
+ }
1239
+ });
1240
+
1241
+ /**
1242
+ * 显示结束页
1243
+ */
1244
+ this.bus.on(MESSAGE_TYPES.PAGE_END_SHOW, function (payload, message, context) {
1245
+ self.emit('page:end:show', payload, message, context);
1246
+ if (self.role === 'game') {
1247
+ self.renderEndPage(payload);
1248
+ }
1249
+ });
1250
+
1251
+ /**
1252
+ * 错误
1253
+ */
1254
+ this.bus.on(MESSAGE_TYPES.ERROR, function (payload, message, context) {
1255
+ self.emit('error', payload, message, context);
1256
+ });
1257
+
1258
+ /**
1259
+ * HEARTBEAT_PING 心跳检测
1260
+ */
1261
+ this.bus.on(MESSAGE_TYPES.HEARTBEAT_PING, function (payload, message, context) {
1262
+ self.emit('heartbeat:ping', payload, message, context);
1263
+ self.send(MESSAGE_TYPES.HEARTBEAT_PONG, {
1264
+ replyTo: message.requestId,
1265
+ time: new Date().getTime()
1266
+ });
1267
+ });
1268
+
1269
+ /**
1270
+ * HEARTBEAT_PONG
1271
+ */
1272
+ this.bus.on(MESSAGE_TYPES.HEARTBEAT_PONG, function (payload, message, context) {
1273
+ self.emit('heartbeat:pong', payload, message, context);
1274
+ });
1275
+ this.bus.on(MESSAGE_TYPES.GAME_RANK, function (payload, message, context) {
1276
+ self.emit('game:rank', payload, message, context);
1277
+ });
1278
+
1279
+ /**
1280
+ * 开始答题
1281
+ */
1282
+ this.bus.on(MESSAGE_TYPES.ANSWER_START, function (payload, message, context) {
1283
+ self.emit('answer:start', payload, message, context);
1284
+ });
1285
+
1286
+ /**
1287
+ * 上一题
1288
+ */
1289
+ this.bus.on(MESSAGE_TYPES.QUESTION_PREV, function (payload, message, context) {
1290
+ self.emit('question:prev', payload, message, context);
1291
+ });
1292
+
1293
+ /**
1294
+ * 下一题
1295
+ */
1296
+ this.bus.on(MESSAGE_TYPES.QUESTION_NEXT, function (payload, message, context) {
1297
+ self.emit('question:next', payload, message, context);
1298
+ });
1299
+
1300
+ /**
1301
+ * 调节声音
1302
+ */
1303
+ this.bus.on(MESSAGE_TYPES.AUDIO_VOLUME_CHANGE, function (payload, message, context) {
1304
+ self.emit('volume:change', payload, message, context);
1305
+ });
1306
+
1307
+ /**
1308
+ * 答题完成
1309
+ */
1310
+ this.bus.on(MESSAGE_TYPES.ANSWER_COMPLETED, function (payload, message, context) {
1311
+ self.emit('answer:completed', payload, message, context);
1312
+ });
1313
+
1314
+ /**
1315
+ * 扩展消息类型
1316
+ */
1317
+ this.bus.on(MESSAGE_TYPES.EXTENSION_EVENT, function (payload, message, context) {
1318
+ self.emit('extension:event', payload, message, context);
1319
+ });
1320
+ };
1321
+
1322
+ /**
1323
+ * 初始化 SDK
1324
+ */
1325
+ GameManager.prototype.init = function () {
1326
+ this.bus.start();
1327
+ if (this.debug && typeof console !== 'undefined' && console.log) {
1328
+ console.log('[GameManager] init, role =', this.role);
1329
+ }
1330
+ return this;
1331
+ };
1332
+
1333
+ /**
1334
+ * 销毁 SDK
1335
+ */
1336
+ GameManager.prototype.destroy = function () {
1337
+ this.state.destroyed = true;
1338
+ this.bus.destroy();
1339
+ if (this.overlay) {
1340
+ if (this.overlay.hideStartPage) {
1341
+ this.overlay.hideStartPage();
1342
+ }
1343
+ if (this.overlay.hideEndPage) {
1344
+ this.overlay.hideEndPage();
1345
+ }
1346
+ }
1347
+ this.off();
1348
+ if (this.debug && typeof console !== 'undefined' && console.log) {
1349
+ console.log('[GameManager] destroyed');
1350
+ }
1351
+ return this;
1352
+ };
1353
+
1354
+ /**
1355
+ * 设置目标窗口
1356
+ * 一般为 iframe.contentWindow
1357
+ */
1358
+ GameManager.prototype.setTargetWindow = function (win) {
1359
+ this.bus.setTargetWindow(win);
1360
+ return this;
1361
+ };
1362
+
1363
+ /**
1364
+ * 设置目标 origin
1365
+ */
1366
+ GameManager.prototype.setTargetOrigin = function (origin) {
1367
+ this.bus.setTargetOrigin(origin);
1368
+ return this;
1369
+ };
1370
+
1371
+ /**
1372
+ * 设置 root
1373
+ */
1374
+ GameManager.prototype.setRoot = function (el) {
1375
+ if (!el) return this;
1376
+ this.root = el;
1377
+ if (this.overlay) {
1378
+ this.overlay.container = el;
1379
+ }
1380
+ return this;
1381
+ };
1382
+
1383
+ /**
1384
+ * 发送消息
1385
+ */
1386
+ GameManager.prototype.send = function (type, payload, meta) {
1387
+ return this.bus.send(type, payload, meta);
1388
+ };
1389
+
1390
+ /**
1391
+ * SDK ready
1392
+ */
1393
+ GameManager.prototype.notifySdkReady = function (extra) {
1394
+ this.state.sdkReady = true;
1395
+ return this.send(MESSAGE_TYPES.SDK_READY, extra || {});
1396
+ };
1397
+
1398
+ /**
1399
+ * 心跳 ping
1400
+ */
1401
+ GameManager.prototype.heartbeatPing = function (extra) {
1402
+ var payload = extra || {};
1403
+ payload.time = new Date().getTime();
1404
+ return this.send(MESSAGE_TYPES.HEARTBEAT_PING, payload);
1405
+ };
1406
+
1407
+ /**
1408
+ * 游戏 ready
1409
+ */
1410
+ GameManager.prototype.gameReady = function (extra) {
1411
+ this.state.gameReady = true;
1412
+ return this.send(MESSAGE_TYPES.GAME_READY, extra || {});
1413
+ };
1414
+
1415
+ /**
1416
+ * 加载游戏
1417
+ */
1418
+ GameManager.prototype.loadGame = function (config) {
1419
+ return this.send(MESSAGE_TYPES.GAME_LOAD, config || {});
1420
+ };
1421
+
1422
+ /**
1423
+ * 游戏加载进度
1424
+ */
1425
+ GameManager.prototype.gameLoading = function (config) {
1426
+ return this.send(MESSAGE_TYPES.GAME_LOADING, config || {});
1427
+ };
1428
+
1429
+ /**
1430
+ * 开始游戏
1431
+ * 保留一个最终版本,避免重复定义覆盖
1432
+ */
1433
+ GameManager.prototype.startGame = function (data) {
1434
+ var payload = data || {};
1435
+ this.state.started = true;
1436
+ this.state.paused = false;
1437
+ this.emit('game:start', payload, null, {
1438
+ local: true
1439
+ });
1440
+ return this.bus.send(MESSAGE_TYPES.GAME_START, payload);
1441
+ };
1442
+
1443
+ /**
1444
+ * 游戏重新开始
1445
+ */
1446
+ GameManager.prototype.restartGame = function (data) {
1447
+ var payload = data || {};
1448
+ this.state.started = true;
1449
+ this.state.paused = false;
1450
+ this.emit('game:restart', payload, null, {
1451
+ local: true
1452
+ });
1453
+ return this.bus.send(MESSAGE_TYPES.GAME_RESTART, payload);
1454
+ };
1455
+
1456
+ /**
1457
+ * 查看游戏排行榜
1458
+ */
1459
+ GameManager.prototype.showGameRank = function (data) {
1460
+ var payload = data || {};
1461
+ return this.bus.send(MESSAGE_TYPES.GAME_RANK, payload);
1462
+ };
1463
+ /**
1464
+ * 暂停游戏
1465
+ */
1466
+ GameManager.prototype.pauseGame = function (data) {
1467
+ this.state.paused = true;
1468
+ return this.send(MESSAGE_TYPES.GAME_PAUSE, data || {});
1469
+ };
1470
+
1471
+ /**
1472
+ * 恢复游戏
1473
+ */
1474
+ GameManager.prototype.resumeGame = function (data) {
1475
+ this.state.paused = false;
1476
+ return this.send(MESSAGE_TYPES.GAME_RESUME, data || {});
1477
+ };
1478
+
1479
+ /**
1480
+ * 游戏结束
1481
+ */
1482
+ GameManager.prototype.finishGame = function (result) {
1483
+ this.state.started = false;
1484
+ return this.send(MESSAGE_TYPES.GAME_FINISH, result || {});
1485
+ };
1486
+
1487
+ /**
1488
+ * 销毁游戏
1489
+ */
1490
+ GameManager.prototype.destroyGame = function (data) {
1491
+ this.state.destroyed = true;
1492
+ return this.send(MESSAGE_TYPES.GAME_DESTROY, data || {});
1493
+ };
1494
+
1495
+ /**
1496
+ * 更新主题
1497
+ * 保持兼容旧接口
1498
+ */
1499
+ GameManager.prototype.updateTheme = function (theme) {
1500
+ if (this.role === 'game') {
1501
+ this.currentTheme = theme || null;
1502
+ }
1503
+ return this.send(MESSAGE_TYPES.THEME_UPDATE, theme || {});
1504
+ };
1505
+
1506
+ /**
1507
+ * 应用主题
1508
+ * 推荐 host 使用这个方法发给 game
1509
+ */
1510
+ GameManager.prototype.applyTheme = function (themeKey, themeConfig, extra) {
1511
+ var payload = extra || {};
1512
+ payload.themeKey = themeKey || '';
1513
+ payload.theme = themeConfig || {};
1514
+ if (this.role === 'game') {
1515
+ this.currentThemeKey = payload.themeKey;
1516
+ this.currentTheme = payload.theme;
1517
+ }
1518
+ return this.send(MESSAGE_TYPES.THEME_UPDATE, payload);
1519
+ };
1520
+
1521
+ /**
1522
+ * 更新配置
1523
+ */
1524
+ GameManager.prototype.updateConfig = function (config) {
1525
+ return this.send(MESSAGE_TYPES.CONFIG_UPDATE, config || {});
1526
+ };
1527
+
1528
+ /**
1529
+ * 显示开始页
1530
+ * host 调这个;真正渲染在 game 内部执行
1531
+ */
1532
+ GameManager.prototype.showStartPage = function (data) {
1533
+ return this.send(MESSAGE_TYPES.PAGE_START_SHOW, data || {});
1534
+ };
1535
+
1536
+ /**
1537
+ * 显示结束页
1538
+ */
1539
+ GameManager.prototype.showEndPage = function (data) {
1540
+ return this.send(MESSAGE_TYPES.PAGE_END_SHOW, data || {});
1541
+ };
1542
+
1543
+ /**
1544
+ * 提交题目
1545
+ */
1546
+ GameManager.prototype.submitQuestion = function (data) {
1547
+ return this.send(MESSAGE_TYPES.QUESTION_SUBMIT, data || {});
1548
+ };
1549
+
1550
+ /**
1551
+ * 上报错误
1552
+ */
1553
+ GameManager.prototype.reportError = function (error) {
1554
+ var payload;
1555
+ if (error instanceof Error) {
1556
+ payload = {
1557
+ message: error.message,
1558
+ stack: error.stack
1559
+ };
1560
+ } else {
1561
+ payload = error || {};
1562
+ }
1563
+ return this.send(MESSAGE_TYPES.ERROR, payload);
1564
+ };
1565
+
1566
+ /**
1567
+ * 获取当前状态
1568
+ */
1569
+ GameManager.prototype.getState = function () {
1570
+ return {
1571
+ sdkReady: this.state.sdkReady,
1572
+ gameReady: this.state.gameReady,
1573
+ started: this.state.started,
1574
+ paused: this.state.paused,
1575
+ destroyed: this.state.destroyed
1576
+ };
1577
+ };
1578
+
1579
+ /**
1580
+ * 是否 ready
1581
+ */
1582
+ GameManager.prototype.isReady = function () {
1583
+ return !!(this.state.sdkReady && this.state.gameReady);
1584
+ };
1585
+
1586
+ /**
1587
+ * 合并页面主题配置
1588
+ * pageKey: startPage / endPage
1589
+ */
1590
+ GameManager.prototype._mergePageTheme = function (pageKey, pageData) {
1591
+ var theme = this.currentTheme || {};
1592
+ var pageTheme = theme[pageKey] || {};
1593
+ var result = {};
1594
+ var key;
1595
+ for (key in pageTheme) {
1596
+ if (Object.prototype.hasOwnProperty.call(pageTheme, key)) {
1597
+ result[key] = pageTheme[key];
1598
+ }
1599
+ }
1600
+ for (key in pageData) {
1601
+ if (Object.prototype.hasOwnProperty.call(pageData, key)) {
1602
+ result[key] = pageData[key];
1603
+ }
1604
+ }
1605
+ return result;
1606
+ };
1607
+
1608
+ /**
1609
+ * game 内部渲染开始页
1610
+ */
1611
+ GameManager.prototype.renderStartPage = function (data, callbacks) {
1612
+ var self = this;
1613
+ var payload = data || {};
1614
+ var finalData = this._mergePageTheme('startPage', payload);
1615
+ if (!this.overlay || !this.overlay.showStartPage) {
1616
+ return this;
1617
+ }
1618
+ this.overlay.showStartPage(finalData, callbacks || {
1619
+ onStart: function onStart() {
1620
+ self.startGame({
1621
+ from: 'sdk-start-game'
1622
+ });
1623
+ }
1624
+ });
1625
+ return this;
1626
+ };
1627
+
1628
+ /**
1629
+ * 隐藏开始页
1630
+ */
1631
+ GameManager.prototype.hideStartPage = function () {
1632
+ if (this.overlay && this.overlay.hideStartPage) {
1633
+ this.overlay.hideStartPage();
1634
+ }
1635
+ return this;
1636
+ };
1637
+
1638
+ /**
1639
+ * game 内部渲染结束页
1640
+ */
1641
+ GameManager.prototype.renderEndPage = function (data, callbacks) {
1642
+ var self = this;
1643
+ var payload = data || {};
1644
+ var finalData = this._mergePageTheme('endPage', payload);
1645
+ if (!this.overlay || !this.overlay.showEndPage) {
1646
+ return this;
1647
+ }
1648
+ this.overlay.showEndPage(finalData, callbacks || {
1649
+ onRestart: function onRestart() {
1650
+ self.restartGame({
1651
+ from: 'sdk-restart-game'
1652
+ });
1653
+ },
1654
+ onShowRank: function onShowRank() {
1655
+ self.showGameRank({
1656
+ from: 'sdk-game-rank'
1657
+ });
1658
+ }
1659
+ });
1660
+ return this;
1661
+ };
1662
+
1663
+ /**
1664
+ * 隐藏结束页
1665
+ */
1666
+ GameManager.prototype.hideEndPage = function () {
1667
+ if (this.overlay && this.overlay.hideEndPage) {
1668
+ this.overlay.hideEndPage();
1669
+ }
1670
+ return this;
1671
+ };
1672
+
1673
+ /**
1674
+ * 直接按 url 加载主题
1675
+ * 更适合 host 使用
1676
+ */
1677
+ GameManager.prototype.loadThemeByUrl = function (url, callback) {
1678
+ loadThemeByUrl(url, callback);
1679
+ return this;
1680
+ };
1681
+
1682
+ /**
1683
+ * 确保主题已加载
1684
+ * 更适合 host 使用
1685
+ */
1686
+ GameManager.prototype.ensureTheme = function (themeKey, themeRoot, callback) {
1687
+ ensureTheme(themeKey, themeRoot, callback);
1688
+ return this;
1689
+ };
1690
+
1691
+ /**
1692
+ * 生成主题地址
1693
+ */
1694
+ GameManager.prototype.buildThemeUrl = function (themeRoot, themeKey) {
1695
+ return buildThemeUrl(themeRoot, themeKey);
1696
+ };
1697
+
1698
+ /**
1699
+ * 游戏完成
1700
+ */
1701
+ GameManager.prototype.gameLoaded = function (config) {
1702
+ return this.send(MESSAGE_TYPES.GAME_LOADED, config || {});
1703
+ };
1704
+ /**
1705
+ * 开始答题
1706
+ */
1707
+ GameManager.prototype.startAnswer = function (data) {
1708
+ return this.send(MESSAGE_TYPES.ANSWER_START, data || {});
1709
+ };
1710
+
1711
+ /**
1712
+ * 上一题
1713
+ */
1714
+ GameManager.prototype.prevQuestion = function (data) {
1715
+ return this.send(MESSAGE_TYPES.QUESTION_PREV, data || {});
1716
+ };
1717
+
1718
+ /**
1719
+ * 下一题
1720
+ */
1721
+ GameManager.prototype.nextQuestion = function (data) {
1722
+ return this.send(MESSAGE_TYPES.QUESTION_NEXT, data || {});
1723
+ };
1724
+
1725
+ /**
1726
+ * 调节声音
1727
+ * 例如 { volume: 0.8, muted: false }
1728
+ */
1729
+ GameManager.prototype.changeVolume = function (data) {
1730
+ return this.send(MESSAGE_TYPES.AUDIO_VOLUME_CHANGE, data || {});
1731
+ };
1732
+
1733
+ /**
1734
+ * 答题完成
1735
+ */
1736
+ GameManager.prototype.completedAnswer = function (data) {
1737
+ return this.send(MESSAGE_TYPES.ANSWER_COMPLETED, data || {});
1738
+ };
1739
+
1740
+ /**
1741
+ * 扩展消息类型
1742
+ */
1743
+ GameManager.prototype.extensionEvent = function (extra) {
1744
+ return this.send(MESSAGE_TYPES.EXTENSION_EVENT, extra || {});
1745
+ };
1746
+
1747
+ var index = {
1748
+ SDK_VERSION: SDK_VERSION,
1749
+ MESSAGE_TYPES: MESSAGE_TYPES,
1750
+ EventEmitter: EventEmitter,
1751
+ MessageBus: MessageBus,
1752
+ GameRuntime: GameManager
1753
+ };
1754
+
1755
+ exports.EventEmitter = EventEmitter;
1756
+ exports.GameRuntime = GameManager;
1757
+ exports.MESSAGE_TYPES = MESSAGE_TYPES;
1758
+ exports.MessageBus = MessageBus;
1759
+ exports.SDK_VERSION = SDK_VERSION;
1760
+ exports.default = index;
1761
+ //# sourceMappingURL=kadou-game-sdk.cjs.map