@maoyugames/phaser-framework 1.0.16 → 1.0.25

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,310 @@
1
+ import { BasePlatform, LocalStorageAdapter, DomLifecycle } from '../chunk-NFEHUFN7.js';
2
+ import { __publicField } from '../chunk-PKBMQBKP.js';
3
+
4
+ // src/platform/impl/youtube/YouTubePlatform.ts
5
+ function hasYT() {
6
+ return typeof ytgame !== "undefined" && !!ytgame;
7
+ }
8
+ function inPlayablesEnv() {
9
+ try {
10
+ return hasYT() && ytgame.IN_PLAYABLES_ENV === true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+ var YouTubeStorage = class {
16
+ constructor() {
17
+ /** 内存缓存:key -> value(string) */
18
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
19
+ /** 是否已预拉(未 hydrate 前 getItem 返回缓存现状,通常为空) */
20
+ __publicField(this, "hydrated", false);
21
+ __publicField(this, "flushTimer");
22
+ /** 非 Playables 环境的本地存储退路 */
23
+ __publicField(this, "fallback", new LocalStorageAdapter());
24
+ }
25
+ /** init 阶段调用:整份预拉存档到内存 */
26
+ async hydrate() {
27
+ if (!inPlayablesEnv()) {
28
+ this.hydrated = true;
29
+ return;
30
+ }
31
+ try {
32
+ const raw = await ytgame.game.loadData();
33
+ if (raw && typeof raw === "string") {
34
+ const obj = JSON.parse(raw);
35
+ if (obj && typeof obj === "object") {
36
+ for (const [k, v] of Object.entries(obj)) this.cache.set(k, String(v));
37
+ }
38
+ }
39
+ } catch {
40
+ }
41
+ this.hydrated = true;
42
+ }
43
+ getItem(key) {
44
+ if (!inPlayablesEnv()) return this.fallback.getItem(key);
45
+ const v = this.cache.get(key);
46
+ return v === void 0 ? null : v;
47
+ }
48
+ setItem(key, value) {
49
+ if (!inPlayablesEnv()) {
50
+ this.fallback.setItem(key, value);
51
+ return;
52
+ }
53
+ this.cache.set(key, value);
54
+ this.scheduleFlush();
55
+ }
56
+ removeItem(key) {
57
+ if (!inPlayablesEnv()) {
58
+ this.fallback.removeItem(key);
59
+ return;
60
+ }
61
+ this.cache.delete(key);
62
+ this.scheduleFlush();
63
+ }
64
+ clear() {
65
+ if (!inPlayablesEnv()) {
66
+ this.fallback.clear();
67
+ return;
68
+ }
69
+ this.cache.clear();
70
+ this.scheduleFlush();
71
+ }
72
+ /** 去抖:合并短时间内多次写,统一整份异步回写 YouTube 存档 */
73
+ scheduleFlush() {
74
+ if (this.flushTimer) clearTimeout(this.flushTimer);
75
+ this.flushTimer = setTimeout(() => void this.flush(), 300);
76
+ }
77
+ async flush() {
78
+ if (!inPlayablesEnv()) return;
79
+ const record = {};
80
+ for (const [k, v] of this.cache.entries()) record[k] = v;
81
+ try {
82
+ await ytgame.game.saveData(JSON.stringify(record));
83
+ } catch {
84
+ try {
85
+ ytgame.health?.logError?.();
86
+ } catch {
87
+ }
88
+ }
89
+ }
90
+ };
91
+ var _YouTubeAuth = class _YouTubeAuth {
92
+ constructor(storage) {
93
+ __publicField(this, "storage");
94
+ this.storage = storage;
95
+ }
96
+ async login() {
97
+ const id = this.ensureAnonId();
98
+ return { code: id, openId: id, raw: { anonymous: true, source: "youtube-local-anon" } };
99
+ }
100
+ // YouTube Playables SDK 无获取用户昵称/头像的 API(身份完全不暴露)→ 恒返 null,游客显示。
101
+ async getProfile() {
102
+ return null;
103
+ }
104
+ // authorize 不实现(YouTube 无显式授权弹窗 / 无 scope 概念);保持 undefined 由可选签名省略。
105
+ /** 读取或生成本地匿名 id(持久化到 storage,使其随 YouTube 存档跟随用户) */
106
+ ensureAnonId() {
107
+ let id = this.storage.getItem(_YouTubeAuth.ANON_ID_KEY);
108
+ if (!id) {
109
+ id = "yt_" + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
110
+ this.storage.setItem(_YouTubeAuth.ANON_ID_KEY, id);
111
+ }
112
+ return id;
113
+ }
114
+ };
115
+ __publicField(_YouTubeAuth, "ANON_ID_KEY", "__yt_anon_id__");
116
+ var YouTubeAuth = _YouTubeAuth;
117
+ var YouTubeAds = class {
118
+ // YouTube 无「预加载」概念(request 即触发);preloadRewarded 作空操作 resolve,保持接口一致。
119
+ async preloadRewarded(_adUnitId) {
120
+ return;
121
+ }
122
+ async showRewarded(adUnitId) {
123
+ if (!inPlayablesEnv() || typeof ytgame.ads?.requestRewardedAd !== "function") {
124
+ return "failed";
125
+ }
126
+ if (!adUnitId) {
127
+ return "failed";
128
+ }
129
+ try {
130
+ const earned = await ytgame.ads.requestRewardedAd(adUnitId);
131
+ return earned ? "completed" : "skipped";
132
+ } catch {
133
+ return "failed";
134
+ }
135
+ }
136
+ async showInterstitial(_adUnitId) {
137
+ if (!inPlayablesEnv() || typeof ytgame.ads?.requestInterstitialAd !== "function") {
138
+ return "failed";
139
+ }
140
+ try {
141
+ await ytgame.ads.requestInterstitialAd();
142
+ return "closed";
143
+ } catch {
144
+ return "failed";
145
+ }
146
+ }
147
+ };
148
+ var YouTubeDevice = class {
149
+ vibrateShort() {
150
+ try {
151
+ navigator.vibrate?.(15);
152
+ } catch {
153
+ }
154
+ }
155
+ vibrateLong() {
156
+ try {
157
+ navigator.vibrate?.(400);
158
+ } catch {
159
+ }
160
+ }
161
+ async setClipboard(text) {
162
+ try {
163
+ if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(text);
164
+ } catch {
165
+ }
166
+ }
167
+ async getClipboard() {
168
+ try {
169
+ if (navigator.clipboard?.readText) return await navigator.clipboard.readText();
170
+ } catch {
171
+ }
172
+ return "";
173
+ }
174
+ // share 不实现(YouTube Playables 无分享 API);业务 device.share?.() 安全跳过。
175
+ };
176
+ var YouTubeLifecycle = class {
177
+ constructor(getLaunchOptions) {
178
+ __publicField(this, "domLifecycle");
179
+ __publicField(this, "getLaunchOptions");
180
+ this.getLaunchOptions = getLaunchOptions;
181
+ this.domLifecycle = new DomLifecycle(getLaunchOptions);
182
+ }
183
+ onShow(cb) {
184
+ if (inPlayablesEnv() && typeof ytgame.system?.onResume === "function") {
185
+ let unset;
186
+ try {
187
+ unset = ytgame.system.onResume(() => cb(this.getLaunchOptions()));
188
+ } catch {
189
+ }
190
+ const domOff = this.domLifecycle.onShow(cb);
191
+ return () => {
192
+ try {
193
+ unset?.();
194
+ } catch {
195
+ }
196
+ domOff();
197
+ };
198
+ }
199
+ return this.domLifecycle.onShow(cb);
200
+ }
201
+ onHide(cb) {
202
+ if (inPlayablesEnv() && typeof ytgame.system?.onPause === "function") {
203
+ let unset;
204
+ try {
205
+ unset = ytgame.system.onPause(() => cb());
206
+ } catch {
207
+ }
208
+ const domOff = this.domLifecycle.onHide(cb);
209
+ return () => {
210
+ try {
211
+ unset?.();
212
+ } catch {
213
+ }
214
+ domOff();
215
+ };
216
+ }
217
+ return this.domLifecycle.onHide(cb);
218
+ }
219
+ };
220
+ var YouTubePlatform = class extends BasePlatform {
221
+ constructor() {
222
+ super(...arguments);
223
+ __publicField(this, "platformName", "youtube");
224
+ __publicField(this, "isMiniGame", true);
225
+ __publicField(this, "ytStorage", new YouTubeStorage());
226
+ /** firstFrameReady / gameReady 仅可调用一次的幂等守卫 */
227
+ __publicField(this, "_firstFrameReady", false);
228
+ __publicField(this, "_gameReady", false);
229
+ __publicField(this, "storage", this.ytStorage);
230
+ __publicField(this, "auth", new YouTubeAuth(this.ytStorage));
231
+ __publicField(this, "ads", new YouTubeAds());
232
+ // payment 不实现(YouTube 禁内购);device.share 不实现(无分享 API)。
233
+ __publicField(this, "device", new YouTubeDevice());
234
+ // shortcut / mission 不实现(YouTube 无对应能力)。
235
+ __publicField(this, "lifecycle", new YouTubeLifecycle(
236
+ () => this.getLaunchOptions()
237
+ ));
238
+ }
239
+ async init() {
240
+ this.readDomSystemInfo();
241
+ this.readDomLaunchOptions();
242
+ if (!inPlayablesEnv()) {
243
+ await this.ytStorage.hydrate();
244
+ return;
245
+ }
246
+ this.callFirstFrameReady();
247
+ try {
248
+ const lang = await ytgame.system.getLanguage();
249
+ if (lang) this._system = { ...this._system, language: lang };
250
+ } catch {
251
+ }
252
+ await this.ytStorage.hydrate();
253
+ try {
254
+ ytgame.system.isAudioEnabled();
255
+ } catch {
256
+ }
257
+ this.callGameReady();
258
+ }
259
+ /**
260
+ * 加载进度上报。YouTube **无** setLoadingProgress API:
261
+ * 这里把对外接口映射为「首次 progress>0 触发 firstFrameReady,progress≥1 触发 gameReady」,
262
+ * 让业务加载场景的进度回调在 YouTube 上也能正确驱动 firstFrameReady/gameReady 时序。
263
+ * @param progress 0..1 的小数
264
+ */
265
+ setLoadingProgress(progress) {
266
+ if (!inPlayablesEnv()) return;
267
+ if (progress > 0) this.callFirstFrameReady();
268
+ if (progress >= 1) this.callGameReady();
269
+ }
270
+ /**
271
+ * 上报排行榜分数(YouTube 平台特有能力,framework 无通用 score 接口)。
272
+ * 业务可经 (App.platform as YouTubePlatform).sendScore(n) 调用。
273
+ * ytgame.engagement.sendScore({value}):value 须为整数且 ≤ Number.MAX_SAFE_INTEGER,否则被拒。
274
+ * 文档:https://developers.google.com/youtube/gaming/playables/reference/sdk#sendscore
275
+ */
276
+ async sendScore(value) {
277
+ if (!inPlayablesEnv() || typeof ytgame.engagement?.sendScore !== "function") return false;
278
+ const v = Math.floor(value);
279
+ if (!Number.isSafeInteger(v)) return false;
280
+ try {
281
+ await ytgame.engagement.sendScore({ value: v });
282
+ return true;
283
+ } catch {
284
+ try {
285
+ ytgame.health?.logError?.();
286
+ } catch {
287
+ }
288
+ return false;
289
+ }
290
+ }
291
+ callFirstFrameReady() {
292
+ if (this._firstFrameReady) return;
293
+ try {
294
+ ytgame.game.firstFrameReady();
295
+ this._firstFrameReady = true;
296
+ } catch {
297
+ }
298
+ }
299
+ callGameReady() {
300
+ if (this._gameReady) return;
301
+ if (!this._firstFrameReady) this.callFirstFrameReady();
302
+ try {
303
+ ytgame.game.gameReady();
304
+ this._gameReady = true;
305
+ } catch {
306
+ }
307
+ }
308
+ };
309
+
310
+ export { YouTubePlatform };
@@ -68,7 +68,7 @@ declare enum LogLevel {
68
68
  */
69
69
 
70
70
  /** 受支持的平台名 */
71
- type PlatformName = 'web' | 'tiktok' | 'wechat' | 'facebook' | 'capacitor';
71
+ type PlatformName = 'web' | 'tiktok' | 'wechat' | 'facebook' | 'capacitor' | 'youtube';
72
72
  /** 平台静态信息 */
73
73
  interface PlatformInfo {
74
74
  /** 平台名 */
@@ -169,6 +169,11 @@ interface IPlatformAuth {
169
169
  nickname: string;
170
170
  avatarUrl: string;
171
171
  } | null>;
172
+ /**
173
+ * 显式授权(弹授权页),返回带额外 scope 的 code,后端换 token 后可拿 nickname/avatar。
174
+ * 平台不支持时为 undefined。用于「按需引导用户授权显示昵称」(如首次进排行榜)。
175
+ */
176
+ authorize?(scope?: string): Promise<LoginResult>;
172
177
  }
173
178
  /** 广告 */
174
179
  type AdResult = 'completed' | 'skipped' | 'failed' | 'closed';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maoyugames/phaser-framework",
3
- "version": "1.0.16",
3
+ "version": "1.0.25",
4
4
  "description": "多平台 Phaser 游戏框架:业务/底层分离,HTTP/WebSocket/KCP,Web/TikTok/微信/Facebook/App 隔离打包",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,6 +36,10 @@
36
36
  "types": "./dist/platform/capacitor.d.ts",
37
37
  "import": "./dist/platform/capacitor.js"
38
38
  },
39
+ "./platform/youtube": {
40
+ "types": "./dist/platform/youtube.d.ts",
41
+ "import": "./dist/platform/youtube.js"
42
+ },
39
43
  "./cli": "./dist/cli/index.js",
40
44
  "./global": {
41
45
  "types": "./global.d.ts"