import { EventEmitter } from '@jolibox/common'; import { TrackEvent } from '@jolibox/types'; type TaskEvent = | 'OPEN_GAME' | 'PLAY_GAME' | 'CLOSE_GAME' | 'COMPLETE_GAME_LEVEL' | 'USE_GAME_ITEM' // TODO: deprecate | 'IN_GAME_PURCHASES' // 游戏内购 | 'ADS_UNLOCK_GAME' // 广告解锁 | 'GAME_TASK_EVENT' // 关卡行为 | 'GAME_ENDED' // 游戏结束 | 'GAME_LEVEL_UP' // 游戏关卡升级 | 'GAME_LEVEL_START' // 游戏关卡开始 | 'GAME_LEVEL_FAIL' // 游戏关卡失败 | 'COMPLETE_WATCH_ADS'; // 完成观看广告 export type TaskPoint = { event: TaskEvent; params?: Record; extraParams?: Record; }; enum ECrossFrameEvent { JOLIBOX_GAME_OPEN = 'JOLIBOX_GAME_OPEN', JOLIBOX_GAME_PLAY = 'JOLIBOX_GAME_PLAY', JOLIBOX_GAME_CLOSE = 'JOLIBOX_GAME_CLOSE' } export abstract class TaskTracker { interval: number; lastReportTime = 0; visible = true; private reportCount = 0; // 添加上报次数计数 protected notifyIframeGameEvent = (eventName: string, data: any) => { const parentWindow = window.parent; if (parentWindow && parentWindow !== window && typeof parentWindow.postMessage === 'function') { parentWindow.postMessage( { type: eventName, data }, '*' ); } }; // 上报逻辑:第1次(5s)、第2次(10s)、第3次(15s),然后每3次(15s)上报一次 // reportCount=1: (5s) // reportCount=2: (10s) // reportCount=3: (15s) // reportCount=4,5: 跳过 // reportCount=6: (30s) // reportCount=7,8: 跳过 // reportCount=9: (45s) private playGame = (duration: number) => { this.reportCount++; const shouldReport = this.reportCount <= 3 || (this.reportCount - 3) % 3 === 0; if (shouldReport) { this.reporter({ event: 'PLAY_GAME', params: { duration } }); this.tracker('PlayGame', { duration }); this.notifyIframeGameEvent(ECrossFrameEvent.JOLIBOX_GAME_PLAY, { duration }); } }; timer = this.createRAFTimer((duration) => { this.playGame(duration); }); abstract reporter(point: TaskPoint): void; abstract tracker(point: TrackEvent, info: Record | null): void; constructor(readonly eventEmitter: EventEmitter<{ visible: [boolean] }>, interval?: number) { this.interval = interval ?? 5 * 1000; eventEmitter.on('visible', (visible) => { this.visible = visible; }); } start(duration?: number) { this.reporter({ event: 'OPEN_GAME', params: { timestamp: Date.now(), duration: duration ?? 0 } }); this.tracker('OpenGame', { duration: duration ?? 0 }); this.notifyIframeGameEvent(ECrossFrameEvent.JOLIBOX_GAME_OPEN, { duration: duration ?? 0 }); this.timer.start(); } close(duration: number) { this.reporter({ event: 'CLOSE_GAME', params: { timestamp: Date.now(), duration } }); this.tracker('CloseGame', { duration }); this.notifyIframeGameEvent(ECrossFrameEvent.JOLIBOX_GAME_CLOSE, { duration }); this.timer.stop(); } private createRAFTimer(callback: (duration: number) => void) { let lastTime = Date.now(); let rafId: number; let intervalId: number; let isRunning = false; let totalTime = 0; const tick = () => { if (!isRunning) return; const currentTime = Date.now(); const deltaTime = currentTime - lastTime; if (deltaTime >= this.interval) { if (this.visible) { // Flutter WebView affects the execution of setInterval timing tasks. Here we do a simple handling. Each totalTime can accumulate at most this.interval + 1s totalTime += Math.min(deltaTime, this.interval + 1000); callback(totalTime); } lastTime = currentTime; } rafId = requestAnimationFrame(tick); }; const startInterval = () => { stopRAF(); lastTime = Date.now(); intervalId = window.setInterval(() => { if (!isRunning) return; const currentTime = Date.now(); const deltaTime = currentTime - lastTime; if (deltaTime >= this.interval) { lastTime = currentTime; } }, this.interval) as unknown as number; }; const startRAF = () => { stopInterval(); lastTime = Date.now(); rafId = requestAnimationFrame(tick); }; const stopRAF = () => { if (rafId) { cancelAnimationFrame(rafId); rafId = 0; } }; const stopInterval = () => { if (intervalId) { clearInterval(intervalId); intervalId = 0; } }; this.eventEmitter.on('visible', (visible) => { if (!isRunning) return; console.log('visible', visible); if (visible) { startRAF(); } else { startInterval(); } }); const originVisible = this.visible; return { start() { if (!isRunning) { isRunning = true; if (originVisible) { startRAF(); } else { startInterval(); } } }, stop() { isRunning = false; stopRAF(); stopInterval(); } }; } }