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