@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,681 @@
1
+ /**
2
+ * 脑龄UI配置接口
3
+ */
4
+ export interface BrainAgeUIConfig {
5
+ scale?: number;
6
+ width?: number;
7
+ height?: number;
8
+ radius?: number;
9
+ padding?: number;
10
+ iconSize?: number;
11
+ iconOffset?: number;
12
+ labelFontSize?: string;
13
+ ageFontSize?: string;
14
+ labelText?: string;
15
+ colors?: {
16
+ bg?: string;
17
+ borderNormal?: string;
18
+ text?: string;
19
+ ageNormal?: string;
20
+ };
21
+ borderWidth?: number;
22
+ }
23
+
24
+ /**
25
+ * 脑龄游戏逻辑配置
26
+ */
27
+ export interface BrainAgeGameConfig {
28
+ /** 初始脑龄 */
29
+ initial?: number;
30
+ /** 最大脑龄(达到则 Game Over) */
31
+ max?: number;
32
+ /** 最小脑龄 */
33
+ min?: number;
34
+ /** 每秒增加的脑龄 */
35
+ incrementPerSecond?: number;
36
+ /** 每次匹配减少的脑龄 */
37
+ decrementPerMatch?: number;
38
+ /** 警告阈值脑龄 */
39
+ warningAge?: number;
40
+ /** 正常状态图标 URL */
41
+ iconUrl?: string;
42
+ /** 警告状态图标 URL */
43
+ warnIconUrl?: string;
44
+ /** 是否启用危险闪烁边框 */
45
+ enableDangerWarning?: boolean;
46
+ /** 脑龄达到上限回调 */
47
+ onGameOver?: () => void;
48
+ }
49
+
50
+ /**
51
+ * 完整配置类型(所有属性都必填,包括嵌套对象)
52
+ */
53
+ type FullBrainAgeUIConfig = Required<Omit<BrainAgeUIConfig, "colors">> & {
54
+ colors: Required<NonNullable<BrainAgeUIConfig["colors"]>>;
55
+ };
56
+
57
+ type FullBrainAgeGameConfig = Required<Omit<BrainAgeGameConfig, 'onGameOver'>>;
58
+
59
+ /**
60
+ * 默认 UI 配置
61
+ */
62
+ const DEFAULT_CONFIG: FullBrainAgeUIConfig = {
63
+ scale: 1,
64
+ width: 400,
65
+ height: 100,
66
+ radius: 50,
67
+ padding: 15,
68
+ iconSize: 75,
69
+ iconOffset: 35,
70
+ labelFontSize: "36px",
71
+ ageFontSize: "72px",
72
+ labelText: "Brain Age:",
73
+ colors: {
74
+ bg: "#ffffff",
75
+ borderNormal: "#dddddd",
76
+ text: "#000000",
77
+ ageNormal: "#2b5c9e",
78
+ },
79
+ borderWidth: 0,
80
+ };
81
+
82
+ /**
83
+ * 默认游戏逻辑配置
84
+ */
85
+ const DEFAULT_GAME_CONFIG: FullBrainAgeGameConfig = {
86
+ initial: 60,
87
+ max: 100,
88
+ min: 20,
89
+ incrementPerSecond: 1,
90
+ decrementPerMatch: 5,
91
+ warningAge: 90,
92
+ iconUrl: '',
93
+ warnIconUrl: '',
94
+ enableDangerWarning: true,
95
+ };
96
+
97
+ /**
98
+ * 脑龄UI组件(纯DOM+CSS实现)
99
+ */
100
+ export class BrainAgeUI {
101
+ private container!: HTMLDivElement;
102
+ private background!: HTMLDivElement;
103
+ private icon?: HTMLElement;
104
+ private ageText!: HTMLDivElement;
105
+ private config: FullBrainAgeUIConfig;
106
+
107
+ // --- 内部状态 ---
108
+ private _age: number = 60;
109
+ private gameConfig: FullBrainAgeGameConfig = { ...DEFAULT_GAME_CONFIG };
110
+ private timer?: number;
111
+ private onGameOver: () => void;
112
+
113
+ // --- 危险警告闪烁 ---
114
+ private dangerOverlay?: HTMLDivElement;
115
+ private isDangerMode: boolean = false;
116
+
117
+ /**
118
+ * @param x X 坐标(像素)
119
+ * @param y Y 坐标(像素)
120
+ * @param iconContent 图标内容(图片 URL 优先,或 emoji/文本,留空不显示)
121
+ * @param uiConfig UI 外观配置
122
+ * @param gameConfig 游戏逻辑配置(含 onGameOver)
123
+ */
124
+ constructor(
125
+ x: number = 30,
126
+ y: number = 30,
127
+ iconContent?: string,
128
+ uiConfig?: BrainAgeUIConfig,
129
+ gameConfig?: BrainAgeGameConfig
130
+ ) {
131
+ this.config = this.mergeConfig(uiConfig);
132
+ this.gameConfig = this.mergeGameConfig(gameConfig);
133
+ this.onGameOver = gameConfig?.onGameOver ?? (() => {});
134
+ this._age = this.gameConfig.initial;
135
+ this.create(x, y, iconContent);
136
+
137
+ // 立即显示初始值
138
+ this.update(this._age);
139
+
140
+ // 初始化危险闪烁边框(如果启用)
141
+ if (this.gameConfig.enableDangerWarning) {
142
+ this.initDangerWarning();
143
+ }
144
+
145
+ // 自动启动计时
146
+ this.start();
147
+ }
148
+
149
+ // ========================================
150
+ // 启动 / 暂停 / 恢复
151
+ // ========================================
152
+
153
+ /**
154
+ * 启动脑龄计时器(每秒 +incrementPerSecond,达到 max 触发 onGameOver)
155
+ */
156
+ public start(): void {
157
+ // 防止重复启动
158
+ if (this.timer) return;
159
+
160
+ this.timer = window.setInterval(() => {
161
+ this._age += this.gameConfig.incrementPerSecond;
162
+ this.update(this._age);
163
+
164
+ // 达到警告值 → 切换图标 + 启动闪烁
165
+ if (this._age >= this.gameConfig.warningAge) {
166
+ this.setIcon(this.gameConfig.warnIconUrl || '⚠️');
167
+ if (!this.isDangerMode) {
168
+ this.isDangerMode = true;
169
+ this.startDangerFlash();
170
+ }
171
+ }
172
+
173
+ // 达到最大值 → 停止闪烁 + Game Over
174
+ if (this._age >= this.gameConfig.max) {
175
+ this.stopDangerFlash();
176
+ if (this.timer) {
177
+ clearInterval(this.timer);
178
+ this.timer = undefined;
179
+ }
180
+ this.onGameOver();
181
+ }
182
+ }, 1000);
183
+ }
184
+
185
+ /**
186
+ * 匹配成功时减少脑龄(内部自管理,自动处理图标切换和闪烁解除)
187
+ * @param customDecrement 自定义减少量(默认使用 gameConfig.decrementPerMatch)
188
+ */
189
+ public onMatchSuccess(customDecrement?: number): void {
190
+ const decrement = customDecrement ?? this.gameConfig.decrementPerMatch;
191
+ this._age = Math.max(this.gameConfig.min, this._age - decrement);
192
+ this.update(this._age);
193
+
194
+ if (this._age < this.gameConfig.warningAge) {
195
+ this.setIcon(this.gameConfig.iconUrl || '🧠');
196
+ this.stopDangerFlash();
197
+ }
198
+ }
199
+
200
+ /**
201
+ * 暂停计时器
202
+ */
203
+ public pause(): void {
204
+ if (this.timer) {
205
+ clearInterval(this.timer);
206
+ this.timer = undefined;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * 恢复计时器
212
+ */
213
+ public resume(): void {
214
+ if (!this.timer) {
215
+ this.start();
216
+ }
217
+ }
218
+
219
+ /**
220
+ * 获取当前脑龄
221
+ */
222
+ public getAge(): number {
223
+ return this._age;
224
+ }
225
+
226
+ /**
227
+ * 外部直接设置脑龄
228
+ */
229
+ public setAge(age: number): void {
230
+ this._age = age;
231
+ this.update(this._age);
232
+
233
+ // 脱离警告区间 → 恢复正常图标 + 停止闪烁
234
+ if (this._age < this.gameConfig.warningAge && this.isDangerMode) {
235
+ this.setIcon(this.gameConfig.iconUrl || '🧠');
236
+ this.stopDangerFlash();
237
+ }
238
+ }
239
+
240
+ // ========================================
241
+ // 内部实现
242
+ // ========================================
243
+
244
+ private mergeGameConfig(userConfig?: BrainAgeGameConfig): FullBrainAgeGameConfig {
245
+ if (!userConfig) return { ...DEFAULT_GAME_CONFIG };
246
+ return {
247
+ initial: userConfig.initial ?? DEFAULT_GAME_CONFIG.initial,
248
+ max: userConfig.max ?? DEFAULT_GAME_CONFIG.max,
249
+ min: userConfig.min ?? DEFAULT_GAME_CONFIG.min,
250
+ incrementPerSecond: userConfig.incrementPerSecond ?? DEFAULT_GAME_CONFIG.incrementPerSecond,
251
+ decrementPerMatch: userConfig.decrementPerMatch ?? DEFAULT_GAME_CONFIG.decrementPerMatch,
252
+ warningAge: userConfig.warningAge ?? DEFAULT_GAME_CONFIG.warningAge,
253
+ iconUrl: userConfig.iconUrl ?? DEFAULT_GAME_CONFIG.iconUrl,
254
+ warnIconUrl: userConfig.warnIconUrl ?? DEFAULT_GAME_CONFIG.warnIconUrl,
255
+ enableDangerWarning: userConfig.enableDangerWarning ?? DEFAULT_GAME_CONFIG.enableDangerWarning,
256
+ };
257
+ }
258
+
259
+ private mergeConfig(userConfig?: BrainAgeUIConfig): FullBrainAgeUIConfig {
260
+ if (!userConfig) {
261
+ return { ...DEFAULT_CONFIG, colors: { ...DEFAULT_CONFIG.colors } };
262
+ }
263
+
264
+ return {
265
+ scale: userConfig.scale ?? DEFAULT_CONFIG.scale,
266
+ width: userConfig.width ?? DEFAULT_CONFIG.width,
267
+ height: userConfig.height ?? DEFAULT_CONFIG.height,
268
+ radius: userConfig.radius ?? DEFAULT_CONFIG.radius,
269
+ padding: userConfig.padding ?? DEFAULT_CONFIG.padding,
270
+ iconSize: userConfig.iconSize ?? DEFAULT_CONFIG.iconSize,
271
+ iconOffset: userConfig.iconOffset ?? DEFAULT_CONFIG.iconOffset,
272
+ labelFontSize: userConfig.labelFontSize ?? DEFAULT_CONFIG.labelFontSize,
273
+ ageFontSize: userConfig.ageFontSize ?? DEFAULT_CONFIG.ageFontSize,
274
+ labelText: userConfig.labelText ?? DEFAULT_CONFIG.labelText,
275
+ colors: {
276
+ bg: userConfig.colors?.bg ?? DEFAULT_CONFIG.colors.bg,
277
+ borderNormal: userConfig.colors?.borderNormal ?? DEFAULT_CONFIG.colors.borderNormal,
278
+ text: userConfig.colors?.text ?? DEFAULT_CONFIG.colors.text,
279
+ ageNormal: userConfig.colors?.ageNormal ?? DEFAULT_CONFIG.colors.ageNormal,
280
+ },
281
+ borderWidth: userConfig.borderWidth ?? DEFAULT_CONFIG.borderWidth,
282
+ };
283
+ }
284
+
285
+ private create(x: number, y: number, iconContent?: string): void {
286
+ const {
287
+ scale: s,
288
+ colors,
289
+ borderWidth: rawBorderWidth,
290
+ labelText,
291
+ } = this.config;
292
+
293
+ const width = this.config.width * s;
294
+ const height = this.config.height * s;
295
+ const radius = this.config.radius * s;
296
+ const padding = this.config.padding * s;
297
+ const iconSize = this.config.iconSize * s;
298
+ const iconOffset = this.config.iconOffset * s;
299
+ const borderWidth = rawBorderWidth * s;
300
+ const labelFontSize = `${parseFloat(this.config.labelFontSize) * s}px`;
301
+ const ageFontSize = `${parseFloat(this.config.ageFontSize) * s}px`;
302
+
303
+ // 创建主容器
304
+ this.container = document.createElement('div');
305
+ this.container.className = 'brainage-ui-container';
306
+
307
+ Object.assign(this.container.style, {
308
+ position: 'fixed',
309
+ left: `${x}px`,
310
+ top: `${y}px`,
311
+ width: `${width}px`,
312
+ height: `${height}px`,
313
+ zIndex: '100',
314
+ transform: 'scale(var(--brainage-ui-scale, 1))',
315
+ transformOrigin: 'top left',
316
+ pointerEvents: 'none',
317
+ userSelect: 'none',
318
+ } as CSSStyleDeclaration);
319
+
320
+ // 创建背景
321
+ this.background = document.createElement('div');
322
+ this.background.className = 'brainage-ui-background';
323
+
324
+ Object.assign(this.background.style, {
325
+ position: 'absolute',
326
+ width: '100%',
327
+ height: '100%',
328
+ backgroundColor: colors.bg,
329
+ border: borderWidth > 0 ? `${borderWidth}px solid ${colors.borderNormal}` : 'none',
330
+ borderRadius: `${radius}px`,
331
+ display: 'flex',
332
+ alignItems: 'center',
333
+ padding: `0 ${padding}px`,
334
+ boxSizing: 'border-box',
335
+ } as CSSStyleDeclaration);
336
+
337
+ // 创建图标(如果提供)
338
+ if (iconContent) {
339
+ // 判断是图片 URL 还是 emoji/文本
340
+ const isImageUrl = iconContent.startsWith('data:image/') || // base64 data URL
341
+ iconContent.startsWith('http') ||
342
+ iconContent.startsWith('/') ||
343
+ iconContent.startsWith('./') ||
344
+ iconContent.includes('.png') ||
345
+ iconContent.includes('.jpg') ||
346
+ iconContent.includes('.webp') ||
347
+ iconContent.includes('.svg');
348
+
349
+ if (isImageUrl) {
350
+ // 使用图片
351
+ const imgElement = document.createElement('img');
352
+ imgElement.className = 'brainage-ui-icon';
353
+ imgElement.src = iconContent;
354
+ imgElement.alt = 'Brain Icon';
355
+
356
+ Object.assign(imgElement.style, {
357
+ width: `${iconSize}px`,
358
+ height: `${iconSize}px`,
359
+ marginRight: `${iconOffset * 0.6}px`, // 调整间距为原值的 0.6 倍
360
+ flexShrink: '0',
361
+ objectFit: 'contain',
362
+ alignSelf: 'center', // 垂直居中对齐
363
+ } as CSSStyleDeclaration);
364
+
365
+ this.icon = imgElement;
366
+ this.background.appendChild(imgElement);
367
+ } else {
368
+ // 使用 emoji/文本
369
+ this.icon = document.createElement('div');
370
+ this.icon.className = 'brainage-ui-icon';
371
+ this.icon.textContent = iconContent;
372
+
373
+ Object.assign(this.icon.style, {
374
+ fontSize: `${iconSize}px`,
375
+ lineHeight: '1', // 改为 1,避免行高冲突
376
+ marginRight: `${iconOffset * 0.6}px`, // 调整间距为原值的 0.6 倍
377
+ flexShrink: '0',
378
+ display: 'flex',
379
+ alignItems: 'center', // 垂直居中对齐
380
+ justifyContent: 'center', // 水平居中对齐
381
+ height: `${iconSize}px`, // 显式设置高度
382
+ } as CSSStyleDeclaration);
383
+
384
+ this.background.appendChild(this.icon);
385
+ }
386
+ }
387
+
388
+ // 创建文本容器
389
+ const textContainer = document.createElement('div');
390
+ textContainer.className = 'brainage-ui-text-container';
391
+ Object.assign(textContainer.style, {
392
+ display: 'flex',
393
+ alignItems: 'center',
394
+ justifyContent: 'space-between',
395
+ flex: '1',
396
+ } as CSSStyleDeclaration);
397
+
398
+ // 创建标签文本
399
+ const labelTextObj = document.createElement('div');
400
+ labelTextObj.className = 'brainage-ui-label';
401
+ labelTextObj.textContent = labelText;
402
+
403
+ Object.assign(labelTextObj.style, {
404
+ fontFamily: 'Arial, sans-serif',
405
+ fontSize: labelFontSize,
406
+ color: colors.text,
407
+ fontWeight: 'bold',
408
+ } as CSSStyleDeclaration);
409
+
410
+ // 创建脑龄文本
411
+ this.ageText = document.createElement('div');
412
+ this.ageText.className = 'brainage-ui-age';
413
+ this.ageText.textContent = this._age.toString();
414
+
415
+ Object.assign(this.ageText.style, {
416
+ fontFamily: 'Arial, sans-serif',
417
+ fontSize: ageFontSize,
418
+ color: colors.ageNormal,
419
+ fontWeight: '700',
420
+ } as CSSStyleDeclaration);
421
+
422
+ textContainer.appendChild(labelTextObj);
423
+ textContainer.appendChild(this.ageText);
424
+ this.background.appendChild(textContainer);
425
+ this.container.appendChild(this.background);
426
+
427
+ document.body.appendChild(this.container);
428
+
429
+ // 响应式处理
430
+ this.setupResponsive();
431
+ }
432
+
433
+ /**
434
+ * 设置响应式缩放
435
+ */
436
+ private setupResponsive(): void {
437
+ const updateScale = () => {
438
+ const currentWidth = window.innerWidth;
439
+ const currentHeight = window.innerHeight;
440
+
441
+ // 判断是否为移动设备
442
+ const isMobile = currentWidth < 768 || (currentWidth < 1024 && currentHeight < 768);
443
+
444
+ let scale: number;
445
+ if (isMobile) {
446
+ // 移动端:基于 375px 基准,最小 0.8 倍,最大 1.2 倍
447
+ const baseWidth = 375;
448
+ scale = Math.max(0.4, Math.min(currentWidth / baseWidth, 0.5));
449
+ } else {
450
+ // PC端:基于 1920px 基准,最小 0.6 倍,最大 1 倍
451
+ const baseWidth = 1920;
452
+ scale = Math.max(0.6, Math.min(currentWidth / baseWidth, 1));
453
+ }
454
+
455
+ document.documentElement.style.setProperty('--brainage-ui-scale', scale.toString());
456
+ };
457
+
458
+ updateScale();
459
+ window.addEventListener('resize', updateScale);
460
+
461
+ // 保存清理函数
462
+ (this.container as any)._cleanupResize = () => {
463
+ window.removeEventListener('resize', updateScale);
464
+ };
465
+ }
466
+
467
+ /**
468
+ * 更新脑龄显示
469
+ */
470
+ public update(age: number): void {
471
+ if (this.ageText) {
472
+ this.ageText.textContent = age.toString();
473
+ }
474
+ }
475
+
476
+ /**
477
+ * 初始化危险警告边框
478
+ */
479
+ public initDangerWarning(): void {
480
+ if (this.dangerOverlay) return; // 避免重复初始化
481
+
482
+ this.dangerOverlay = document.createElement('div');
483
+ this.dangerOverlay.className = 'brainage-danger-overlay';
484
+
485
+ Object.assign(this.dangerOverlay.style, {
486
+ position: 'fixed',
487
+ left: '0',
488
+ top: '0',
489
+ width: '100vw',
490
+ height: '100vh',
491
+ border: '8px solid #ff3333',
492
+ boxSizing: 'border-box',
493
+ pointerEvents: 'none',
494
+ zIndex: '99',
495
+ opacity: '0',
496
+ display: 'none',
497
+ } as CSSStyleDeclaration);
498
+
499
+ document.body.appendChild(this.dangerOverlay);
500
+ }
501
+
502
+ /**
503
+ * 启动危险警告闪烁(使用 CSS 动画)
504
+ */
505
+ private startDangerFlash(): void {
506
+ if (!this.dangerOverlay) return;
507
+
508
+ // 注入 CSS 动画(如果尚未注入)
509
+ if (!document.getElementById('brainage-danger-keyframes')) {
510
+ const style = document.createElement('style');
511
+ style.id = 'brainage-danger-keyframes';
512
+ style.textContent = `
513
+ @keyframes brainage-danger-flash {
514
+ 0%, 100% { opacity: 0; }
515
+ 50% { opacity: 0.6; }
516
+ }
517
+ `;
518
+ document.head.appendChild(style);
519
+ }
520
+
521
+ this.dangerOverlay.style.display = 'block';
522
+ this.dangerOverlay.style.animation = 'brainage-danger-flash 1s ease-in-out infinite';
523
+ }
524
+
525
+ public stopDangerFlash(): void {
526
+ if (this.dangerOverlay) {
527
+ this.dangerOverlay.style.display = 'none';
528
+ this.dangerOverlay.style.animation = '';
529
+ }
530
+ this.isDangerMode = false;
531
+ }
532
+
533
+ /**
534
+ * 切换图标
535
+ */
536
+ public setIcon(iconContent: string): void {
537
+ if (!this.icon) return;
538
+
539
+ // 判断是图片 URL 还是 emoji/文本
540
+ const isImageUrl = iconContent.startsWith('data:image/') || // base64 data URL
541
+ iconContent.startsWith('http') ||
542
+ iconContent.startsWith('/') ||
543
+ iconContent.startsWith('./') ||
544
+ iconContent.includes('.png') ||
545
+ iconContent.includes('.jpg') ||
546
+ iconContent.includes('.webp') ||
547
+ iconContent.includes('.svg');
548
+
549
+ if (isImageUrl) {
550
+ // 如果是 img 元素,直接更新 src
551
+ if (this.icon instanceof HTMLImageElement) {
552
+ this.icon.src = iconContent;
553
+ } else {
554
+ // 如果原来是 div,需要替换为 img
555
+ const parent = this.icon.parentElement;
556
+ if (parent) {
557
+ const imgElement = document.createElement('img');
558
+ imgElement.className = 'brainage-ui-icon';
559
+ imgElement.src = iconContent;
560
+ imgElement.alt = 'Brain Icon';
561
+
562
+ const computedStyle = getComputedStyle(this.icon);
563
+ Object.assign(imgElement.style, {
564
+ width: computedStyle.width,
565
+ height: computedStyle.height,
566
+ marginRight: computedStyle.marginRight,
567
+ flexShrink: '0',
568
+ objectFit: 'contain',
569
+ alignSelf: 'center', // 垂直居中对齐
570
+ } as CSSStyleDeclaration);
571
+
572
+ parent.replaceChild(imgElement, this.icon);
573
+ this.icon = imgElement;
574
+ }
575
+ }
576
+ } else {
577
+ // 如果是 div 元素,直接更新文本
578
+ if (this.icon instanceof HTMLDivElement) {
579
+ this.icon.textContent = iconContent;
580
+ } else {
581
+ // 如果原来是 img,需要替换为 div
582
+ const parent = this.icon.parentElement;
583
+ if (parent) {
584
+ const divElement = document.createElement('div');
585
+ divElement.className = 'brainage-ui-icon';
586
+ divElement.textContent = iconContent;
587
+
588
+ const computedStyle = getComputedStyle(this.icon);
589
+ Object.assign(divElement.style, {
590
+ fontSize: computedStyle.width, // 使用宽度作为字体大小
591
+ lineHeight: '1',
592
+ height: computedStyle.height,
593
+ marginRight: computedStyle.marginRight,
594
+ flexShrink: '0',
595
+ display: 'flex',
596
+ alignItems: 'center',
597
+ justifyContent: 'center',
598
+ } as CSSStyleDeclaration);
599
+
600
+ parent.replaceChild(divElement, this.icon);
601
+ this.icon = divElement;
602
+ }
603
+ }
604
+ }
605
+ }
606
+
607
+ /**
608
+ * Game Over 遮罩 + 面板放大居中
609
+ */
610
+ public showGameOver(gameOverScale: number = 1.5, overlayAlpha: number = 0.6): void {
611
+ const s = this.config.scale;
612
+ const extraW = 30 * s;
613
+ const baseW = this.config.width * s;
614
+ const newW = baseW + extraW;
615
+
616
+ // 创建遮罩层
617
+ const overlay = document.createElement('div');
618
+ overlay.className = 'brainage-gameover-overlay';
619
+
620
+ Object.assign(overlay.style, {
621
+ position: 'fixed',
622
+ left: '0',
623
+ top: '0',
624
+ width: '100vw',
625
+ height: '100vh',
626
+ backgroundColor: `rgba(0, 0, 0, ${overlayAlpha})`,
627
+ zIndex: '300',
628
+ } as CSSStyleDeclaration);
629
+
630
+ document.body.appendChild(overlay);
631
+
632
+ // 更新背景样式
633
+ this.background.style.width = `${newW}px`;
634
+ this.background.style.border = `${6 * s}px solid #ff4444`;
635
+
636
+ // 更新脑龄文本颜色
637
+ this.ageText.style.color = '#ff4444';
638
+
639
+ // 放大并居中容器
640
+ this.container.style.transform = `scale(${gameOverScale * parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--brainage-ui-scale') || '1')})`;
641
+ this.container.style.left = '50%';
642
+ this.container.style.top = '50%';
643
+ this.container.style.transform += ' translate(-50%, -60%)';
644
+ this.container.style.zIndex = '301';
645
+ }
646
+
647
+ public setPosition(x: number, y: number): void {
648
+ this.container.style.left = `${x}px`;
649
+ this.container.style.top = `${y}px`;
650
+ }
651
+
652
+ public setVisible(visible: boolean): void {
653
+ this.container.style.display = visible ? 'block' : 'none';
654
+ }
655
+
656
+ public getConfig(): FullBrainAgeUIConfig {
657
+ return { ...this.config };
658
+ }
659
+
660
+ public destroy(): void {
661
+ if (this.timer) {
662
+ clearInterval(this.timer);
663
+ this.timer = undefined;
664
+ }
665
+
666
+ this.stopDangerFlash();
667
+
668
+ // 清理响应式监听器
669
+ const cleanup = (this.container as any)._cleanupResize;
670
+ if (cleanup) cleanup();
671
+
672
+ // 移除 DOM 元素
673
+ if (this.container) {
674
+ this.container.remove();
675
+ }
676
+
677
+ if (this.dangerOverlay) {
678
+ this.dangerOverlay.remove();
679
+ }
680
+ }
681
+ }