import { sys } from 'cc'; /** 小游戏宿主类型(与 window 全局 API 对象一一对应) */ export enum MiniGameKind { None = 'none', WeChat = 'wx', Douyin = 'tt', Kuaishou = 'ks', Bilibili = 'bl', Oppo = 'qg', QQ = 'qq', } declare global { interface Window { wx?: MiniGameHostApi; tt?: MiniGameHostApi; ks?: MiniGameHostApi; bl?: MiniGameHostApi; qg?: MiniGameHostApi; qq?: MiniGameHostApi; } } /** 各平台 SDK 共有能力的最小接口(扩展字段均为可选) */ export interface MiniGameHostApi { getSystemInfoSync?: () => SystemInfoLike; createRewardedVideoAd?: (opts: { adUnitId: string }) => RewardedVideoAdLike; createInterstitialAd?: (opts: { adUnitId: string }) => InterstitialAdLike; onShow?: (cb: (res?: Record) => void) => void; onHide?: (cb: () => void) => void; onMemoryWarning?: (cb: (res?: { level?: number }) => void) => void; triggerGC?: () => void; vibrateShort?: (opts?: object) => void; vibrateLong?: (opts?: object) => void; reportScene?: (opts: { sceneId: number; costTime?: number; success?: (res: unknown) => void; fail?: (res: unknown) => void }) => void; login?: (opts: object) => void; shareAppMessage?: (opts: object) => void; onShareAppMessage?: (cb: () => object) => void; showShareMenu?: (opts: object) => void; getUpdateManager?: () => UpdateManagerLike; showModal?: (opts: object) => void; showToast?: (opts: object) => void; restartMiniProgramSync?: () => void; getUserInfo?: (opts: object) => void; getLogManager?: () => { log: (msg: string) => void; debug: (msg: string) => void }; getLaunchOptionsSync?: () => Record; canIUse?: (api: string) => boolean; checkFeedSubscribeStatus?: (opts: object) => void; requestFeedSubscribe?: (opts: object) => void; navigateToScene?: (opts: object) => void; checkSliderBarIsAvailable?: (opts: object) => void; addShortcut?: (opts: object) => void; getPrivacySetting?: (opts: object) => void; requirePrivacyAuthorize?: (opts: object) => void; [key: string]: unknown; } interface UpdateManagerLike { onUpdateReady?: (cb: (res: unknown) => void) => void; onUpdateFailed?: (cb: (err: unknown) => void) => void; onCheckForUpdate?: () => void; applyUpdate?: () => void; } interface SystemInfoLike { platform?: string; system?: string; SDKVersion?: string; brand?: string; model?: string; } interface RewardedVideoAdLike { load?: () => Promise; show?: () => Promise; onLoad?: (cb: () => void) => void; onError?: (cb: (err: unknown) => void) => void; onClose?: (cb: (res?: { isEnded?: boolean }) => void) => void; offLoad?: (cb: () => void) => void; offError?: (cb: (err: unknown) => void) => void; offClose?: (cb: (res?: { isEnded?: boolean }) => void) => void; destroy?: () => void; } interface InterstitialAdLike { load?: () => Promise; show?: () => Promise; onClose?: (cb: () => void) => void; destroy?: () => void; } /** * 小游戏平台识别与能力查询。 * 以 Cocos sys.platform 为准,window 全局仅作 API 绑定;避免 ks/tt/wx 优先级不一致。 */ export class MiniGamePlatform { private static _inited = false; private static _kind = MiniGameKind.None; private static _pf: MiniGameHostApi | null = null; private static _isHarmonyOS = false; private static _systemInfo: SystemInfoLike | null = null; static init(): void { if (this._inited) return; this._inited = true; this._kind = this._detectKind(); this._pf = this._bindPf(this._kind); this._systemInfo = this._readSystemInfo(); this._isHarmonyOS = this._detectHarmonyOS(this._systemInfo); console.log(`[MiniGamePlatform] kind=${this._kind} harmony=${this._isHarmonyOS}`); } static get kind(): MiniGameKind { this.init(); return this._kind; } static get pf(): MiniGameHostApi | null { this.init(); return this._pf; } static get isHarmonyOS(): boolean { this.init(); return this._isHarmonyOS; } static get systemInfo(): SystemInfoLike | null { this.init(); return this._systemInfo; } /** 抖音 / 快手 / B 站:全屏激励视频,平台挂起 JS,禁止 game.pause() */ static isFullScreenRewardedVideoHost(): boolean { const k = this.kind; return k === MiniGameKind.Douyin || k === MiniGameKind.Kuaishou || k === MiniGameKind.Bilibili; } /** 微信 overlay 激励视频:需 game.pause() 冻结主循环 */ static shouldPauseEngineForVideoAd(): boolean { return this.kind === MiniGameKind.WeChat; } /** * 微信 / 快手:createRewardedVideoAd 为全局单例,禁止 destroy 后频繁重建。 * @see https://developers.weixin.qq.com/minigame/dev/guide/open-ability/ad/rewarded-video-ad.html * @see https://open.kuaishou.com/miniGameDocs/gameDev/open-function/ad.html */ static isRewardedVideoAdSingleton(): boolean { const k = this.kind; return k === MiniGameKind.WeChat || k === MiniGameKind.Kuaishou; } /** * B 站激励视频展示前上报 sceneId=1007(官方示例要求)。 * @see https://miniapp.bilibili.com/small-game-doc/open/ad/IncentiveVideo */ static reportRewardedVideoScene(): void { if (this.kind !== MiniGameKind.Bilibili) return; try { this.pf?.reportScene?.({ sceneId: 1007 }); } catch (e) { console.warn('[MiniGamePlatform] reportScene(1007) failed', e); } } /** 激励视频是否正常播完(各平台 onClose 语义) */ static isRewardedVideoCompleted(res?: { isEnded?: boolean } | null): boolean { if (res == null) { // 微信 <2.1.0 基础库 res 为 undefined 表示完整观看;快手无此兼容 return this.kind === MiniGameKind.WeChat; } return !!res.isEnded; } /** * 官方兼容性:优先 canIUse,其次函数是否存在。 * @see https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/basic-library/compatibility-description/ */ static canIUse(api: string): boolean { this.init(); const pf = this._pf; if (!pf) return false; try { if (typeof pf.canIUse === 'function') { return !!pf.canIUse(api); } } catch { /* ignore */ } return typeof (pf as Record)[api] === 'function'; } /** * 安全调用异步开放 API(鸿蒙/低版本不支持时走 fail,不抛同步异常阻塞业务)。 * 官方:不支持时 errNo=10302,应避免直接调用。 * @see https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/tutorial/open-capabilities/general-capabilities/miniapp-harmony-adapter */ static invokeAsync( api: string, opts: Record = {}, ): boolean { this.init(); const pf = this._pf; const fail = typeof opts.fail === 'function' ? opts.fail as (err: unknown) => void : null; if (!pf || !this.canIUse(api)) { fail?.({ errNo: 10302, errorCode: 88, errMsg: `${api}:fail The feature is not available in current operating system`, }); return false; } const fn = (pf as Record)[api]; if (typeof fn !== 'function') { fail?.({ errNo: 10302, errorCode: 88, errMsg: `${api}:fail not a function`, }); return false; } try { (fn as (o: object) => void).call(pf, opts); return true; } catch (e) { fail?.(e); return false; } } /** 鸿蒙上视频挂载/录屏分享等能力常不可用,调用前用此判断做降级 */ static isDouyinVideoOpenCapabilityAvailable(api: string): boolean { if (this.kind !== MiniGameKind.Douyin) return this.canIUse(api); if (this.isHarmonyOS && !this.canIUse(api)) return false; return this.canIUse(api); } private static _detectKind(): MiniGameKind { switch (sys.platform) { case sys.Platform.WECHAT_GAME: return MiniGameKind.WeChat; case sys.Platform.BYTEDANCE_MINI_GAME: return MiniGameKind.Douyin; default: break; } // 非 Cocos 枚举覆盖的宿主(快手/B 站/OPPO/QQ)按 window 全局识别 if (typeof window !== 'undefined') { if (window.ks) return MiniGameKind.Kuaishou; if (window.bl) return MiniGameKind.Bilibili; if (window.tt) return MiniGameKind.Douyin; if (window.qg) return MiniGameKind.Oppo; if (window.qq) return MiniGameKind.QQ; if (window.wx) return MiniGameKind.WeChat; } return MiniGameKind.None; } private static _bindPf(kind: MiniGameKind): MiniGameHostApi | null { if (typeof window === 'undefined') return null; switch (kind) { case MiniGameKind.WeChat: return window.wx ?? null; case MiniGameKind.Douyin: return window.tt ?? null; case MiniGameKind.Kuaishou: return window.ks ?? null; case MiniGameKind.Bilibili: return window.bl ?? null; case MiniGameKind.Oppo: return window.qg ?? null; case MiniGameKind.QQ: return window.qq ?? null; default: return null; } } private static _readSystemInfo(): SystemInfoLike | null { try { return this._pf?.getSystemInfoSync?.() ?? null; } catch { return null; } } /** * 抖音鸿蒙识别(OpenHarmony / HarmonyOS NEXT)。 * 官方建议优先用 system.includes('openharmony')(真机+IDE 通用); * platform==='openHarmony' 仅真机可靠。 * @see https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/tutorial/open-capabilities/general-capabilities/miniapp-harmony-adapter */ private static _detectHarmonyOS(info: SystemInfoLike | null): boolean { if (!info) return false; const system = (info.system ?? '').toLowerCase(); if (system.includes('openharmony')) return true; return (info.platform ?? '') === 'openHarmony' || (info.platform ?? '').toLowerCase() === 'openharmony'; } }