@playcraft/cli 0.0.52 → 0.0.55

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.
Files changed (79) hide show
  1. package/dist/commands/create.js +164 -98
  2. package/dist/index.js +4 -1
  3. package/dist/scaffold/templates.js +35 -0
  4. package/package.json +3 -3
  5. package/project-templates/playable-template-phaser/.playcraftignore +10 -0
  6. package/project-templates/playable-template-phaser/CLAUDE.md +178 -0
  7. package/project-templates/playable-template-phaser/PLAYCRAFT-MIGRATION.md +328 -0
  8. package/project-templates/playable-template-phaser/README.md +381 -0
  9. package/project-templates/playable-template-phaser/assets/brain.png +0 -0
  10. package/project-templates/playable-template-phaser/assets/button.png +0 -0
  11. package/project-templates/playable-template-phaser/assets/interact.png +0 -0
  12. package/project-templates/playable-template-phaser/assets/logo.webp +0 -0
  13. package/project-templates/playable-template-phaser/assets/warn_brain.png +0 -0
  14. package/project-templates/playable-template-phaser/builds.config.js +12 -0
  15. package/project-templates/playable-template-phaser/globals.d.ts +2 -0
  16. package/project-templates/playable-template-phaser/index.html +12 -0
  17. package/project-templates/playable-template-phaser/package.json +45 -0
  18. package/project-templates/playable-template-phaser/scripts/base64-to-mp3.js +340 -0
  19. package/project-templates/playable-template-phaser/scripts/convert-to-webp.js +370 -0
  20. package/project-templates/playable-template-phaser/src/Game.ts +154 -0
  21. package/project-templates/playable-template-phaser/src/config/LoadingConfig.ts +40 -0
  22. package/project-templates/playable-template-phaser/src/config/ThemeConfig.ts +81 -0
  23. package/project-templates/playable-template-phaser/src/index.css +18 -0
  24. package/project-templates/playable-template-phaser/src/index.html +9 -0
  25. package/project-templates/playable-template-phaser/src/index.ts +30 -0
  26. package/project-templates/playable-template-phaser/src/json5.d.ts +4 -0
  27. package/project-templates/playable-template-phaser/src/managers/AudioManager.ts +559 -0
  28. package/project-templates/playable-template-phaser/src/scenes/MainScene.ts +301 -0
  29. package/project-templates/playable-template-phaser/src/scenes/PreloadScene.ts +256 -0
  30. package/project-templates/playable-template-phaser/src/theme/README.md +194 -0
  31. package/project-templates/playable-template-phaser/src/theme/blue-theme/assets/.gitkeep +24 -0
  32. package/project-templates/playable-template-phaser/src/theme/blue-theme/index.ts +27 -0
  33. package/project-templates/playable-template-phaser/src/theme/blue-theme/theme.data.json5 +19 -0
  34. package/project-templates/playable-template-phaser/src/theme/green-theme/assets/.gitkeep +3 -0
  35. package/project-templates/playable-template-phaser/src/theme/green-theme/index.ts +20 -0
  36. package/project-templates/playable-template-phaser/src/theme/green-theme/theme.data.json5 +19 -0
  37. package/project-templates/playable-template-phaser/src/theme/index.ts +1 -0
  38. package/project-templates/playable-template-phaser/src/theme/red-theme/assets/.gitkeep +3 -0
  39. package/project-templates/playable-template-phaser/src/theme/red-theme/index.ts +20 -0
  40. package/project-templates/playable-template-phaser/src/theme/red-theme/theme.data.json5 +19 -0
  41. package/project-templates/playable-template-phaser/src/theme/theme.metadata.md +60 -0
  42. package/project-templates/playable-template-phaser/src/theme/theme.schema.json5 +149 -0
  43. package/project-templates/playable-template-phaser/src/ui/BrainAgeUI.ts +681 -0
  44. package/project-templates/playable-template-phaser/src/ui/CountdownUI.ts +565 -0
  45. package/project-templates/playable-template-phaser/src/ui/PlayButton.ts +51 -0
  46. package/project-templates/playable-template-phaser/src/ui/ScoreUI.ts +207 -0
  47. package/project-templates/playable-template-phaser/src/ui/SuccessUI.ts +43 -0
  48. package/project-templates/playable-template-phaser/src/ui/index.ts +15 -0
  49. package/project-templates/playable-template-phaser/tsconfig.json +26 -0
  50. package/project-templates/playable-template-phaser/vite.config.ts +64 -0
  51. package/project-templates/playable-template-phaser//346/226/260/346/211/213/345/274/200/345/217/221/346/214/207/345/215/227.md +209 -0
  52. package/project-templates/playable-template-three/README.md +795 -0
  53. package/project-templates/playable-template-three/assets/Stork.glb +0 -0
  54. package/project-templates/playable-template-three/assets/button.png +0 -0
  55. package/project-templates/playable-template-three/assets/logo.webp +0 -0
  56. package/project-templates/playable-template-three/build.json +7 -0
  57. package/project-templates/playable-template-three/builds.config.example.js +151 -0
  58. package/project-templates/playable-template-three/builds.config.js +158 -0
  59. package/project-templates/playable-template-three/package.json +36 -0
  60. package/project-templates/playable-template-three/scripts/convert-to-webp.js +370 -0
  61. package/project-templates/playable-template-three/scripts/theme-builds.js +514 -0
  62. package/project-templates/playable-template-three/src/Game.ts +215 -0
  63. package/project-templates/playable-template-three/src/config/LoadingConfig.ts +27 -0
  64. package/project-templates/playable-template-three/src/config/ThemeConfig.ts +36 -0
  65. package/project-templates/playable-template-three/src/global.d.ts +108 -0
  66. package/project-templates/playable-template-three/src/index.css +18 -0
  67. package/project-templates/playable-template-three/src/index.html +9 -0
  68. package/project-templates/playable-template-three/src/index.ts +20 -0
  69. package/project-templates/playable-template-three/src/managers/AudioManager.ts +538 -0
  70. package/project-templates/playable-template-three/src/scenes/PreloadScene.ts +305 -0
  71. package/project-templates/playable-template-three/src/theme/README.md +225 -0
  72. package/project-templates/playable-template-three/src/theme/blue-theme/assets/.gitkeep +28 -0
  73. package/project-templates/playable-template-three/src/theme/blue-theme/index.ts +45 -0
  74. package/project-templates/playable-template-three/src/theme/current.ts +12 -0
  75. package/project-templates/playable-template-three/src/theme/green-theme/assets/.gitkeep +3 -0
  76. package/project-templates/playable-template-three/src/theme/green-theme/index.ts +34 -0
  77. package/project-templates/playable-template-three/src/theme/red-theme/assets/.gitkeep +3 -0
  78. package/project-templates/playable-template-three/src/theme/red-theme/index.ts +34 -0
  79. package/project-templates/playable-template-three/tsconfig.json +24 -0
@@ -0,0 +1,565 @@
1
+ /**
2
+ * 倒计时UI配置接口
3
+ */
4
+ export interface CountdownUIConfig {
5
+ /** 初始倒计时秒数 */
6
+ initialSeconds?: number;
7
+ /** 倒计时归零回调 */
8
+ onGameOver?: () => void;
9
+ bgWidth?: number;
10
+ bgHeight?: number;
11
+ bgRadius?: number;
12
+ bgAlpha?: number;
13
+ bgColor?: string;
14
+ digitWidth?: number;
15
+ digitHeight?: number;
16
+ digitSpacing?: number;
17
+ colonWidth?: number;
18
+ dotSize?: number;
19
+ dotSpacing?: number;
20
+ colorNormal?: string;
21
+ colorWarn?: string;
22
+ colorOff?: string;
23
+ warnTime?: number;
24
+ glowAlpha?: number;
25
+ /** 是否启用危险闪烁边框 */
26
+ enableDangerWarning?: boolean;
27
+ }
28
+
29
+ /**
30
+ * 完整配置类型(所有属性都必填,onGameOver 除外)
31
+ */
32
+ type FullCountdownUIConfig = Required<Omit<CountdownUIConfig, 'onGameOver'>>;
33
+
34
+ /**
35
+ * 默认配置
36
+ */
37
+ const DEFAULT_CONFIG: FullCountdownUIConfig = {
38
+ initialSeconds: 30,
39
+ bgWidth: 200,
40
+ bgHeight: 60,
41
+ bgRadius: 12,
42
+ bgAlpha: 0.6,
43
+ bgColor: '#0a0a0a',
44
+ digitWidth: 24,
45
+ digitHeight: 38,
46
+ digitSpacing: 6,
47
+ colonWidth: 20,
48
+ dotSize: 3,
49
+ dotSpacing: 10,
50
+ colorNormal: '#00ff00',
51
+ colorWarn: '#ff3333',
52
+ colorOff: '#003300',
53
+ warnTime: 10,
54
+ glowAlpha: 0.3,
55
+ enableDangerWarning: true,
56
+ };
57
+
58
+ /** 推荐的默认倒计时(秒) */
59
+ export const DEFAULT_COUNTDOWN = 30;
60
+
61
+ /** 推荐的默认警告阈值(秒) */
62
+ export const DEFAULT_WARN_TIME = 10;
63
+
64
+ /**
65
+ * LED数码管风格倒计时UI组件(纯DOM+CSS实现)
66
+ * ```
67
+ */
68
+ export class CountdownUI {
69
+ private container!: HTMLDivElement;
70
+ private background!: HTMLDivElement;
71
+ private digitCanvases: HTMLCanvasElement[] = [];
72
+ private config: FullCountdownUIConfig;
73
+
74
+
75
+ // --- 内部状态 ---
76
+ private _countdown: number = 0;
77
+ private timer?: number;
78
+ private onGameOver: () => void;
79
+
80
+ // --- 危险警告闪烁 ---
81
+ private dangerOverlay?: HTMLDivElement;
82
+ private isDangerMode: boolean = false;
83
+
84
+ /**
85
+ * @param x 中心 X 坐标
86
+ * @param y 中心 Y 坐标
87
+ * @param config UI 配置(含 initialSeconds、onGameOver 等)
88
+ */
89
+ constructor(x: number = 0, y: number = 0, config?: CountdownUIConfig) {
90
+ this.config = this.mergeConfig(config);
91
+ this.onGameOver = config?.onGameOver ?? (() => {});
92
+ this._countdown = this.config.initialSeconds;
93
+ this.create(x, y);
94
+
95
+ // 立即显示初始值
96
+ this.update(this._countdown);
97
+
98
+ // 初始化危险闪烁边框(如果启用)
99
+ if (this.config.enableDangerWarning) {
100
+ this.initDangerWarning();
101
+ }
102
+
103
+ // 自动启动倒计时
104
+ this.start();
105
+ }
106
+
107
+ // ========================================
108
+ // 启动 / 暂停 / 恢复
109
+ // ========================================
110
+
111
+ /**
112
+ * 启动倒计时(每秒 -1,到 0 触发 onGameOver)
113
+ * 构造时已完成初始化和显示,此方法只负责"开始走时间"
114
+ */
115
+ public start(): void {
116
+ // 防止重复启动
117
+ if (this.timer) return;
118
+
119
+ this.timer = window.setInterval(() => {
120
+ this._countdown -= 1;
121
+ this.update(this._countdown);
122
+
123
+ if (this._countdown <= this.config.warnTime && !this.isDangerMode) {
124
+ this.isDangerMode = true;
125
+ this.startDangerFlash();
126
+ }
127
+
128
+ if (this._countdown <= 0) {
129
+ this.stopDangerFlash();
130
+ if (this.timer) {
131
+ clearInterval(this.timer);
132
+ this.timer = undefined;
133
+ }
134
+ this.onGameOver();
135
+ }
136
+ }, 1000);
137
+ }
138
+
139
+ /**
140
+ * 暂停倒计时
141
+ */
142
+ public pause(): void {
143
+ if (this.timer) {
144
+ clearInterval(this.timer);
145
+ this.timer = undefined;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * 恢复倒计时
151
+ */
152
+ public resume(): void {
153
+ if (!this.timer) {
154
+ this.start();
155
+ }
156
+ }
157
+
158
+ /**
159
+ * 获取当前剩余秒数
160
+ */
161
+ public getCountdown(): number {
162
+ return this._countdown;
163
+ }
164
+
165
+ /**
166
+ * 外部直接设置剩余秒数(如加时奖励)
167
+ */
168
+ public setCountdown(seconds: number): void {
169
+ this._countdown = seconds;
170
+ this.update(this._countdown);
171
+
172
+ // 如果加时后脱离警告区间,停止闪烁
173
+ if (this._countdown > this.config.warnTime && this.isDangerMode) {
174
+ this.stopDangerFlash();
175
+ }
176
+ }
177
+
178
+ // ========================================
179
+ // 内部实现
180
+ // ========================================
181
+
182
+ /**
183
+ * 合并配置
184
+ */
185
+ private mergeConfig(userConfig?: CountdownUIConfig): FullCountdownUIConfig {
186
+ if (!userConfig) {
187
+ return { ...DEFAULT_CONFIG };
188
+ }
189
+
190
+ return {
191
+ initialSeconds: userConfig.initialSeconds ?? DEFAULT_CONFIG.initialSeconds,
192
+ bgWidth: userConfig.bgWidth ?? DEFAULT_CONFIG.bgWidth,
193
+ bgHeight: userConfig.bgHeight ?? DEFAULT_CONFIG.bgHeight,
194
+ bgRadius: userConfig.bgRadius ?? DEFAULT_CONFIG.bgRadius,
195
+ bgAlpha: userConfig.bgAlpha ?? DEFAULT_CONFIG.bgAlpha,
196
+ bgColor: userConfig.bgColor ?? DEFAULT_CONFIG.bgColor,
197
+ digitWidth: userConfig.digitWidth ?? DEFAULT_CONFIG.digitWidth,
198
+ digitHeight: userConfig.digitHeight ?? DEFAULT_CONFIG.digitHeight,
199
+ digitSpacing: userConfig.digitSpacing ?? DEFAULT_CONFIG.digitSpacing,
200
+ colonWidth: userConfig.colonWidth ?? DEFAULT_CONFIG.colonWidth,
201
+ dotSize: userConfig.dotSize ?? DEFAULT_CONFIG.dotSize,
202
+ dotSpacing: userConfig.dotSpacing ?? DEFAULT_CONFIG.dotSpacing,
203
+ colorNormal: userConfig.colorNormal ?? DEFAULT_CONFIG.colorNormal,
204
+ colorWarn: userConfig.colorWarn ?? DEFAULT_CONFIG.colorWarn,
205
+ colorOff: userConfig.colorOff ?? DEFAULT_CONFIG.colorOff,
206
+ warnTime: userConfig.warnTime ?? DEFAULT_CONFIG.warnTime,
207
+ glowAlpha: userConfig.glowAlpha ?? DEFAULT_CONFIG.glowAlpha,
208
+ enableDangerWarning: userConfig.enableDangerWarning ?? DEFAULT_CONFIG.enableDangerWarning,
209
+ };
210
+ }
211
+
212
+ private create(x: number, y: number): void {
213
+ // 创建主容器
214
+ this.container = document.createElement('div');
215
+ this.container.className = 'countdown-ui-container';
216
+
217
+ // 创建半透明背景
218
+ this.background = document.createElement('div');
219
+ this.background.className = 'countdown-ui-background';
220
+
221
+ const { bgWidth, bgHeight, bgRadius, bgAlpha, bgColor } = this.config;
222
+
223
+ // 将十六进制颜色转为 rgba
224
+ const rgba = `${bgColor}${Math.round(bgAlpha * 255).toString(16).padStart(2, '0')}`;
225
+
226
+ Object.assign(this.background.style, {
227
+ position: 'absolute',
228
+ width: `${bgWidth}px`,
229
+ height: `${bgHeight}px`,
230
+ backgroundColor: rgba,
231
+ borderRadius: `${bgRadius}px`,
232
+ left: '50%',
233
+ top: '50%',
234
+ transform: 'translate(-50%, -50%)',
235
+ } as CSSStyleDeclaration);
236
+
237
+ this.container.appendChild(this.background);
238
+
239
+ // 创建数码管容器
240
+ const digitsContainer = document.createElement('div');
241
+ digitsContainer.className = 'countdown-digits';
242
+ Object.assign(digitsContainer.style, {
243
+ position: 'absolute',
244
+ left: '50%',
245
+ top: '50%',
246
+ transform: 'translate(-50%, -50%)',
247
+ display: 'flex',
248
+ alignItems: 'center',
249
+ gap: `${this.config.digitSpacing}px`,
250
+ } as CSSStyleDeclaration);
251
+
252
+ // 创建5个 canvas:分钟十位、分钟个位、冒号、秒钟十位、秒钟个位
253
+ for (let i = 0; i < 5; i++) {
254
+ const canvas = document.createElement('canvas');
255
+ canvas.className = i === 2 ? 'countdown-colon' : 'countdown-digit';
256
+
257
+ if (i === 2) {
258
+ // 冒号
259
+ canvas.width = this.config.colonWidth;
260
+ canvas.height = this.config.digitHeight;
261
+ } else {
262
+ // 数字
263
+ canvas.width = this.config.digitWidth;
264
+ canvas.height = this.config.digitHeight;
265
+ }
266
+
267
+ this.digitCanvases.push(canvas);
268
+ digitsContainer.appendChild(canvas);
269
+ }
270
+
271
+ this.container.appendChild(digitsContainer);
272
+
273
+ // 设置容器样式(使用传入的 x, y 坐标)
274
+ Object.assign(this.container.style, {
275
+ position: 'fixed',
276
+ left: `${x}px`,
277
+ top: `${y}px`,
278
+ transform: 'translateX(-50%) scale(var(--countdown-ui-scale, 1))', // translateX(-50%) 使元素以 x 为中心
279
+ transformOrigin: 'top center',
280
+ zIndex: '100',
281
+ pointerEvents: 'none',
282
+ userSelect: 'none',
283
+ } as CSSStyleDeclaration);
284
+
285
+ document.body.appendChild(this.container);
286
+
287
+ // 响应式处理
288
+ this.setupResponsive();
289
+ }
290
+
291
+ /**
292
+ * 设置响应式缩放(参考 ScoreUI)
293
+ */
294
+ private setupResponsive(): void {
295
+ const updateScale = () => {
296
+ const currentWidth = window.innerWidth;
297
+ const currentHeight = window.innerHeight;
298
+
299
+ // 判断是否为移动设备
300
+ const isMobile = currentWidth < 768 || (currentWidth < 1024 && currentHeight < 768);
301
+
302
+ let scale: number;
303
+ if (isMobile) {
304
+ // 移动端:基于 375px 基准,最小 0.8 倍,最大 1.2 倍
305
+ const baseWidth = 375;
306
+ scale = Math.max(0.4, Math.min(currentWidth / baseWidth, 0.5));
307
+ } else {
308
+ // PC端:基于 1920px 基准,最小 0.6 倍,最大 1 倍
309
+ const baseWidth = 1920;
310
+ scale = Math.max(0.6, Math.min(currentWidth / baseWidth, 1));
311
+ }
312
+
313
+ document.documentElement.style.setProperty('--countdown-ui-scale', scale.toString());
314
+ };
315
+
316
+ updateScale();
317
+ window.addEventListener('resize', updateScale);
318
+
319
+ // 保存清理函数
320
+ (this.container as any)._cleanupResize = () => {
321
+ window.removeEventListener('resize', updateScale);
322
+ };
323
+ }
324
+
325
+ /**
326
+ * 更新倒计时显示
327
+ */
328
+ public update(seconds: number): void {
329
+ const mins = Math.floor(seconds / 60);
330
+ const secs = seconds % 60;
331
+
332
+ const color = seconds <= this.config.warnTime ? this.config.colorWarn : this.config.colorNormal;
333
+
334
+ this.drawDigit(this.digitCanvases[0], Math.floor(mins / 10), color);
335
+ this.drawDigit(this.digitCanvases[1], mins % 10, color);
336
+ this.drawColon(this.digitCanvases[2], color);
337
+ this.drawDigit(this.digitCanvases[3], Math.floor(secs / 10), color);
338
+ this.drawDigit(this.digitCanvases[4], secs % 10, color);
339
+ }
340
+
341
+ /**
342
+ * 初始化危险警告边框
343
+ */
344
+ public initDangerWarning(): void {
345
+ if (this.dangerOverlay) return; // 避免重复初始化
346
+
347
+ this.dangerOverlay = document.createElement('div');
348
+ this.dangerOverlay.className = 'countdown-danger-overlay';
349
+
350
+ Object.assign(this.dangerOverlay.style, {
351
+ position: 'fixed',
352
+ left: '0',
353
+ top: '0',
354
+ width: '100vw',
355
+ height: '100vh',
356
+ border: '8px solid #ff3333',
357
+ boxSizing: 'border-box',
358
+ pointerEvents: 'none',
359
+ zIndex: '99',
360
+ opacity: '0',
361
+ display: 'none',
362
+ } as CSSStyleDeclaration);
363
+
364
+ document.body.appendChild(this.dangerOverlay);
365
+ }
366
+
367
+ /**
368
+ * 启动危险警告闪烁(使用 CSS 动画)
369
+ */
370
+ private startDangerFlash(): void {
371
+ if (!this.dangerOverlay) return;
372
+
373
+ // 注入 CSS 动画(如果尚未注入)
374
+ if (!document.getElementById('countdown-danger-keyframes')) {
375
+ const style = document.createElement('style');
376
+ style.id = 'countdown-danger-keyframes';
377
+ style.textContent = `
378
+ @keyframes countdown-danger-flash {
379
+ 0%, 100% { opacity: 0; }
380
+ 50% { opacity: 0.6; }
381
+ }
382
+ `;
383
+ document.head.appendChild(style);
384
+ }
385
+
386
+ this.dangerOverlay.style.display = 'block';
387
+ this.dangerOverlay.style.animation = 'countdown-danger-flash 1s ease-in-out infinite';
388
+ }
389
+
390
+ /**
391
+ * 停止危险警告闪烁
392
+ */
393
+ public stopDangerFlash(): void {
394
+ if (this.dangerOverlay) {
395
+ this.dangerOverlay.style.display = 'none';
396
+ this.dangerOverlay.style.animation = '';
397
+ }
398
+ this.isDangerMode = false;
399
+ }
400
+
401
+ /**
402
+ * 绘制七段数码管数字(使用 Canvas)
403
+ */
404
+ private drawDigit(canvas: HTMLCanvasElement, digit: number, color: string): void {
405
+ const ctx = canvas.getContext('2d');
406
+ if (!ctx) return;
407
+
408
+ const width = canvas.width;
409
+ const height = canvas.height;
410
+
411
+ ctx.clearRect(0, 0, width, height);
412
+
413
+ // 七段数码管映射(上、右上、右下、下、左下、左上、中)
414
+ const segments: { [key: number]: boolean[] } = {
415
+ 0: [true, true, true, true, true, true, false],
416
+ 1: [false, true, true, false, false, false, false],
417
+ 2: [true, true, false, true, true, false, true],
418
+ 3: [true, true, true, true, false, false, true],
419
+ 4: [false, true, true, false, false, true, true],
420
+ 5: [true, false, true, true, false, true, true],
421
+ 6: [true, false, true, true, true, true, true],
422
+ 7: [true, true, true, false, false, false, false],
423
+ 8: [true, true, true, true, true, true, true],
424
+ 9: [true, true, true, true, false, true, true]
425
+ };
426
+
427
+ const segs = segments[digit] || segments[0];
428
+ const segW = width * 0.7;
429
+ const segH = height * 0.12;
430
+ const segVW = segH;
431
+ const segVH = (height - segH * 3) / 2;
432
+ const gap = 2;
433
+
434
+ const drawSegment = (cx: number, cy: number, w: number, h: number, isHorizontal: boolean, isOn: boolean) => {
435
+ ctx.globalAlpha = isOn ? 1 : 0.1;
436
+ ctx.fillStyle = isOn ? color : this.config.colorOff;
437
+
438
+ ctx.beginPath();
439
+ if (isHorizontal) {
440
+ // 水平段(六边形)
441
+ ctx.moveTo(cx - w / 2 + h / 2, cy - h / 2);
442
+ ctx.lineTo(cx + w / 2 - h / 2, cy - h / 2);
443
+ ctx.lineTo(cx + w / 2, cy);
444
+ ctx.lineTo(cx + w / 2 - h / 2, cy + h / 2);
445
+ ctx.lineTo(cx - w / 2 + h / 2, cy + h / 2);
446
+ ctx.lineTo(cx - w / 2, cy);
447
+ } else {
448
+ // 垂直段(六边形)
449
+ ctx.moveTo(cx, cy - w / 2);
450
+ ctx.lineTo(cx + h / 2, cy - w / 2 + h / 2);
451
+ ctx.lineTo(cx + h / 2, cy + w / 2 - h / 2);
452
+ ctx.lineTo(cx, cy + w / 2);
453
+ ctx.lineTo(cx - h / 2, cy + w / 2 - h / 2);
454
+ ctx.lineTo(cx - h / 2, cy - w / 2 + h / 2);
455
+ }
456
+ ctx.closePath();
457
+ ctx.fill();
458
+ };
459
+
460
+ const halfW = width / 2;
461
+ const halfH = height / 2;
462
+
463
+ // 绘制七段(顺序:上、右上、右下、下、左下、左上、中)
464
+ drawSegment(halfW, segH / 2 + gap, segW, segH, true, segs[0]); // 上
465
+ drawSegment(width - segVW / 2 - gap, halfH / 2, segVH, segVW, false, segs[1]); // 右上
466
+ drawSegment(width - segVW / 2 - gap, halfH + halfH / 2, segVH, segVW, false, segs[2]); // 右下
467
+ drawSegment(halfW, height - segH / 2 - gap, segW, segH, true, segs[3]); // 下
468
+ drawSegment(segVW / 2 + gap, halfH + halfH / 2, segVH, segVW, false, segs[4]); // 左下
469
+ drawSegment(segVW / 2 + gap, halfH / 2, segVH, segVW, false, segs[5]); // 左上
470
+ drawSegment(halfW, halfH, segW, segH, true, segs[6]); // 中
471
+
472
+ ctx.globalAlpha = 1;
473
+ }
474
+
475
+ /**
476
+ * 绘制数码管冒号(使用 Canvas)
477
+ */
478
+ private drawColon(canvas: HTMLCanvasElement, color: string): void {
479
+ const ctx = canvas.getContext('2d');
480
+ if (!ctx) return;
481
+
482
+ const width = canvas.width;
483
+ const height = canvas.height;
484
+ const { dotSize, dotSpacing } = this.config;
485
+
486
+ ctx.clearRect(0, 0, width, height);
487
+ ctx.fillStyle = color;
488
+
489
+ // 上点
490
+ ctx.beginPath();
491
+ ctx.arc(width / 2, height / 2 - dotSpacing, dotSize, 0, Math.PI * 2);
492
+ ctx.fill();
493
+
494
+ // 下点
495
+ ctx.beginPath();
496
+ ctx.arc(width / 2, height / 2 + dotSpacing, dotSize, 0, Math.PI * 2);
497
+ ctx.fill();
498
+ }
499
+
500
+ /**
501
+ * Game Over 时绘制遮罩层,在屏幕中央显示 "Time Up!" 文字
502
+ */
503
+ public showGameOver(overlayAlpha: number = 0.6, overlayDepth: number = 300): void {
504
+ const overlay = document.createElement('div');
505
+ overlay.className = 'countdown-gameover-overlay';
506
+
507
+ Object.assign(overlay.style, {
508
+ position: 'fixed',
509
+ left: '0',
510
+ top: '0',
511
+ width: '100vw',
512
+ height: '100vh',
513
+ backgroundColor: `rgba(0, 0, 0, ${overlayAlpha})`,
514
+ zIndex: overlayDepth.toString(),
515
+ display: 'flex',
516
+ alignItems: 'center',
517
+ justifyContent: 'center',
518
+ } as CSSStyleDeclaration);
519
+
520
+ const text = document.createElement('div');
521
+ text.textContent = 'Time Up!';
522
+ Object.assign(text.style, {
523
+ fontFamily: 'Arial, sans-serif',
524
+ fontSize: '72px',
525
+ color: '#ffffff',
526
+ fontWeight: 'bold',
527
+ textShadow: '0 0 4px #000000, 0 0 8px #000000',
528
+ } as CSSStyleDeclaration);
529
+
530
+ overlay.appendChild(text);
531
+ document.body.appendChild(overlay);
532
+ }
533
+
534
+ /**
535
+ * 获取配置
536
+ */
537
+ public getConfig(): FullCountdownUIConfig {
538
+ return { ...this.config };
539
+ }
540
+
541
+ /**
542
+ * 销毁组件
543
+ */
544
+ public destroy(): void {
545
+ if (this.timer) {
546
+ clearInterval(this.timer);
547
+ this.timer = undefined;
548
+ }
549
+
550
+ this.stopDangerFlash();
551
+
552
+ // 清理响应式监听器
553
+ const cleanup = (this.container as any)._cleanupResize;
554
+ if (cleanup) cleanup();
555
+
556
+ // 移除 DOM 元素
557
+ if (this.container) {
558
+ this.container.remove();
559
+ }
560
+
561
+ if (this.dangerOverlay) {
562
+ this.dangerOverlay.remove();
563
+ }
564
+ }
565
+ }
@@ -0,0 +1,51 @@
1
+ import * as Phaser from 'phaser';
2
+ import { sdk } from '@playcraft/adsdk';
3
+
4
+ /**
5
+ * 通用 Play 按钮组件
6
+ * 使用 interact 合图中的 playBtn 帧,点击跳转安装,带呼吸动画
7
+ *
8
+ * 纹理依赖:
9
+ * - interact 合图中的 playBtn 帧
10
+ */
11
+ export class PlayButton {
12
+ private scene: Phaser.Scene;
13
+ private btn!: Phaser.GameObjects.Image;
14
+ private isGoogleChannel: boolean;
15
+
16
+ constructor(scene: Phaser.Scene) {
17
+ this.scene = scene;
18
+ this.isGoogleChannel = (window as any).PlayableSDK?.getCurChannel?.() === 'google';
19
+ }
20
+
21
+ /**
22
+ * 在指定位置显示按钮
23
+ * @param x x 坐标
24
+ * @param y y 坐标
25
+ * @param scale 缩放(默认 0.45)
26
+ * @param depth 渲染层级(默认 301)
27
+ */
28
+ public show(x: number, y: number, scale: number = 0.45, depth: number = 301): Phaser.GameObjects.Image {
29
+ this.btn = this.scene.add.image(x, y, 'interact', 'playBtn')
30
+ .setScale(scale)
31
+ .setDepth(depth)
32
+ .setInteractive({ useHandCursor: true });
33
+
34
+ this.btn.on('pointerdown', () => sdk.playcraftInstall());
35
+
36
+ // 呼吸动画(Google 渠道禁用)
37
+ if (!this.isGoogleChannel) {
38
+ this.scene.tweens.add({
39
+ targets: this.btn,
40
+ scaleX: scale * 1.1,
41
+ scaleY: scale * 1.1,
42
+ duration: 700,
43
+ yoyo: true,
44
+ repeat: -1,
45
+ ease: 'Sine.easeInOut'
46
+ });
47
+ }
48
+
49
+ return this.btn;
50
+ }
51
+ }