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