eithney commited on
Commit
11eb8d2
·
verified ·
1 Parent(s): 7353e4f

Upload 3 files

Browse files
Files changed (3) hide show
  1. main.js +1415 -0
  2. style.css +1164 -0
  3. voice.js +667 -0
main.js ADDED
@@ -0,0 +1,1415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 主游戏类
2
+ class MagicPotGame {
3
+ constructor() {
4
+ this.canvas = document.getElementById('gameCanvas');
5
+ this.ctx = this.canvas.getContext('2d');
6
+ this.gameState = 'idle'; // idle, activated, cooking, overflowing, stopped
7
+ this.currentFood = null;
8
+ this.foodCount = 0;
9
+ // this.particles = []; // 不再需要单独的粒子数组,使用particleSystem管理
10
+ this.lastTime = 0;
11
+
12
+ // 游戏配置
13
+ this.config = {
14
+ maxFoodCount: 100, // 最大食物计数
15
+ maxParticles: 150, // 最大粒子数量
16
+ maxAnimals: 8, // 最大动物数量
17
+ animalSpawnThreshold: 5, // 动物出现阈值(降低到5以便更快测试)
18
+ easterEggThreshold: 30, // 彩蛋触发阈值
19
+ particleSpawnRate: 1, // 粒子生成速率(调整为1,降低食物生成速度)
20
+ cookingDuration: 3000, // 3秒烹饪时间
21
+ cleanupInterval: 5000, // 清理间隔(毫秒)
22
+ maxGroundedParticles: 80 // 地面最大粒子数
23
+ };
24
+
25
+ // 初始化组件
26
+ this.voice = new VoiceRecognition();
27
+ this.keyboard = new KeyboardInputHandler();
28
+ this.pot = new MagicPot(this.canvas.width / 2, this.canvas.height / 2 + 50);
29
+ this.particleSystem = new ParticleSystem();
30
+ this.animalSystem = new AnimalSystem(this.canvas); // 传入Canvas引用
31
+ this.textToImage = new TextToImageAPI();
32
+ this.audio = window.audioManager;
33
+
34
+ // 食物生成控制
35
+ this.lastFoodSpawnTime = 0;
36
+ this.foodSpawnInterval = 500; // 每500毫秒生成一次食物(显著降低生成速度)
37
+
38
+ // 性能监控
39
+ this.lastCleanupTime = 0;
40
+ this.performanceStats = {
41
+ particles: 0,
42
+ animals: 0,
43
+ fps: 0,
44
+ lastFpsTime: 0,
45
+ frameCount: 0
46
+ };
47
+
48
+ this.init();
49
+ }
50
+
51
+ init() {
52
+ this.setupEventListeners();
53
+ this.setupCanvas();
54
+ this.gameLoop();
55
+ this.updateUI();
56
+
57
+ // 显示初始提示
58
+ this.showDebug('游戏初始化完成,等待语音命令...');
59
+
60
+ // 启动定期清理
61
+ this.startPerformanceMonitoring();
62
+ }
63
+
64
+ setupEventListeners() {
65
+ // 语音识别事件
66
+ this.voice.onResult = (transcript) => {
67
+ this.handleVoiceCommand(transcript);
68
+ };
69
+
70
+ this.voice.onError = (error) => {
71
+ this.showDebug(`语音识别错误: ${error}`);
72
+ };
73
+
74
+ this.voice.onStatusChange = (status) => {
75
+ this.updateVoiceStatus(status);
76
+ };
77
+
78
+ // 键盘输入事件
79
+ this.keyboard.onCommand = (command) => {
80
+ this.handleKeyboardCommand(command);
81
+ };
82
+
83
+ // 键盘调试快捷键
84
+ document.addEventListener('keydown', (e) => {
85
+ if (e.key === 'F12') {
86
+ this.toggleDebug();
87
+ } else if (e.key === 'F11') {
88
+ this.testAudio();
89
+ } else if (e.key === 'F10') {
90
+ this.toggleAudio();
91
+ } else if (e.key === 'F9') {
92
+ e.preventDefault(); // 确保阻止浏览器默认行为
93
+ this.toggleKeyboardActivation();
94
+ } else if (e.key === 'F8') {
95
+ this.toggleKeyboardPanel();
96
+ } else if (e.key === 'F7') {
97
+ this.clearCommandHistory();
98
+ } else if (e.key === 'F6') {
99
+ this.restartVoice();
100
+ } else if (e.key === 'F3') {
101
+ this.manualActivateVoice();
102
+ } else if (e.key === 'F5') {
103
+ this.resetKeyboardPanelPosition();
104
+ } else if (e.key === 'F4') {
105
+ this.adjustFoodSpawnSpeed();
106
+ } else if (e.key === 'a' && e.altKey) {
107
+ e.preventDefault();
108
+ this.forceSpawnAnimal();
109
+ }
110
+ });
111
+
112
+ // 音效按钮
113
+ const audioButton = document.getElementById('audioButton');
114
+ if (audioButton) {
115
+ audioButton.addEventListener('click', () => {
116
+ this.toggleAudio();
117
+ });
118
+ this.updateAudioButton();
119
+ }
120
+
121
+ // 语音激活按钮
122
+ const voiceActivateButton = document.getElementById('voiceActivateButton');
123
+ if (voiceActivateButton) {
124
+ voiceActivateButton.addEventListener('click', () => {
125
+ this.manualActivateVoice();
126
+ });
127
+ // 初始化按钮状态
128
+ this.updateVoiceActivateButton();
129
+ }
130
+
131
+ // 帮助按钮
132
+ const helpButton = document.getElementById('helpButton');
133
+ if (helpButton) {
134
+ helpButton.addEventListener('click', () => {
135
+ this.showVoiceHelp();
136
+ });
137
+ }
138
+
139
+ // 窗口大小调整
140
+ window.addEventListener('resize', () => {
141
+ this.resizeCanvas();
142
+ });
143
+ }
144
+
145
+ setupCanvas() {
146
+ // 设置高DPI显示
147
+ const dpr = window.devicePixelRatio || 1;
148
+ const rect = this.canvas.getBoundingClientRect();
149
+
150
+ this.canvas.width = rect.width * dpr;
151
+ this.canvas.height = rect.height * dpr;
152
+ this.ctx.scale(dpr, dpr);
153
+
154
+ this.canvas.style.width = rect.width + 'px';
155
+ this.canvas.style.height = rect.height + 'px';
156
+ }
157
+
158
+ handleVoiceCommand(transcript) {
159
+ const command = transcript.toLowerCase().trim();
160
+ this.showDebug(`语音命令: "${command}"`);
161
+ this.processCommand(command, '语音');
162
+ }
163
+
164
+ handleKeyboardCommand(command) {
165
+ const normalizedCommand = command.toLowerCase().trim();
166
+ this.showDebug(`键盘命令: "${command}"`);
167
+ this.processCommand(normalizedCommand, '键盘');
168
+ }
169
+
170
+ processCommand(command, source) {
171
+ // 统一的命令处理逻辑
172
+
173
+ // 激活魔法锅 - 更灵活的匹配
174
+ if (this.matchActivationCommand(command)) {
175
+ this.activatePot();
176
+ this.showDebug(`通过${source}激活魔法锅`);
177
+ return;
178
+ }
179
+
180
+ // 停止烹饪 - 更灵活的匹配
181
+ if (this.matchStopCommand(command)) {
182
+ this.stopCooking();
183
+ this.showDebug(`通过${source}停止烹饪`);
184
+ return;
185
+ }
186
+
187
+ // 如果锅已激活,处理食物命令
188
+ if (this.gameState === 'activated') {
189
+ const foodName = this.extractFoodName(command);
190
+ if (foodName) {
191
+ this.startCooking(foodName);
192
+ this.showDebug(`通过${source}开始烹饪: ${foodName}`);
193
+ } else {
194
+ this.showDebug(`未识别的食物: "${command}" (来源: ${source})`);
195
+ }
196
+ } else {
197
+ // 如果锅未激活,提示用户先激活
198
+ if (this.isValidFoodCommand(command)) {
199
+ this.showDebug(`请先激活魔法锅再烹饪食物 (来源: ${source})`);
200
+ this.showSpecialMessage('请先说 "cook cook cook pot" 激活魔法锅');
201
+ }
202
+ }
203
+ }
204
+
205
+ isValidFoodCommand(command) {
206
+ // 检查是否是有效的食物命令
207
+ const foods = [
208
+ 'apple', 'banana', 'orange', 'strawberry', 'watermelon', 'grape',
209
+ 'pizza', 'burger', 'bread', 'rice', 'noodles', 'cake', 'cookie',
210
+ 'cheese', 'fish', 'chicken', 'carrot', 'tomato', 'corn', 'broccoli',
211
+ 'potato', 'onion'
212
+ ];
213
+
214
+ return foods.some(food => command.includes(food));
215
+ }
216
+
217
+ matchActivationCommand(command) {
218
+ // 多种激活命令的匹配模式
219
+ const patterns = [
220
+ /cook.*cook.*cook.*pot/,
221
+ /cook.*pot.*cook.*cook/,
222
+ /pot.*cook.*cook.*cook/,
223
+ /cook\s+cook\s+cook\s+pot/,
224
+ /cook.*three.*times.*pot/,
225
+ /magic.*pot.*cook/
226
+ ];
227
+
228
+ return patterns.some(pattern => pattern.test(command));
229
+ }
230
+
231
+ matchStopCommand(command) {
232
+ // 多种停止命令的匹配模式
233
+ const patterns = [
234
+ /stop.*stop.*stop.*pot/,
235
+ /stop.*pot.*stop.*stop/,
236
+ /pot.*stop.*stop.*stop/,
237
+ /stop\s+stop\s+stop\s+pot/,
238
+ /stop.*three.*times.*pot/,
239
+ /magic.*pot.*stop/
240
+ ];
241
+
242
+ return patterns.some(pattern => pattern.test(command));
243
+ }
244
+
245
+ extractFoodName(command) {
246
+ // 支持的食物列表
247
+ const foods = [
248
+ 'apple', 'banana', 'orange', 'strawberry', 'watermelon', 'grape',
249
+ 'pizza', 'burger', 'bread', 'rice', 'noodles', 'cake', 'cookie',
250
+ 'cheese', 'fish', 'chicken', 'carrot', 'tomato', 'corn', 'broccoli',
251
+ 'potato', 'onion'
252
+ ];
253
+
254
+ // 在命令中查找食物名称
255
+ for (const food of foods) {
256
+ if (command.includes(food)) {
257
+ return food;
258
+ }
259
+ }
260
+
261
+ // 如果没有找到确切匹配,尝试模糊匹配
262
+ for (const food of foods) {
263
+ if (this.fuzzyMatch(command, food)) {
264
+ return food;
265
+ }
266
+ }
267
+
268
+ return null;
269
+ }
270
+
271
+ fuzzyMatch(text, target) {
272
+ // 简单的模糊匹配算法
273
+ const threshold = 0.7;
274
+ const similarity = this.calculateSimilarity(text, target);
275
+ return similarity >= threshold;
276
+ }
277
+
278
+ calculateSimilarity(str1, str2) {
279
+ // 计算两个字符串的相似度
280
+ const longer = str1.length > str2.length ? str1 : str2;
281
+ const shorter = str1.length > str2.length ? str2 : str1;
282
+
283
+ if (longer.length === 0) return 1.0;
284
+
285
+ const distance = this.levenshteinDistance(longer, shorter);
286
+ return (longer.length - distance) / longer.length;
287
+ }
288
+
289
+ levenshteinDistance(str1, str2) {
290
+ // 计算编辑距离
291
+ const matrix = [];
292
+
293
+ for (let i = 0; i <= str2.length; i++) {
294
+ matrix[i] = [i];
295
+ }
296
+
297
+ for (let j = 0; j <= str1.length; j++) {
298
+ matrix[0][j] = j;
299
+ }
300
+
301
+ for (let i = 1; i <= str2.length; i++) {
302
+ for (let j = 1; j <= str1.length; j++) {
303
+ if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
304
+ matrix[i][j] = matrix[i - 1][j - 1];
305
+ } else {
306
+ matrix[i][j] = Math.min(
307
+ matrix[i - 1][j - 1] + 1,
308
+ matrix[i][j - 1] + 1,
309
+ matrix[i - 1][j] + 1
310
+ );
311
+ }
312
+ }
313
+ }
314
+
315
+ return matrix[str2.length][str1.length];
316
+ }
317
+
318
+ activatePot() {
319
+ if (this.gameState === 'idle') {
320
+ this.gameState = 'activated';
321
+ this.pot.activate();
322
+ this.updateUI();
323
+ this.showDebug('魔法锅已激活!');
324
+ this.showSpecialMessage('🪄 魔法锅已激活!\n现在说出食物名称开始烹饪');
325
+
326
+ // 播放激活音效
327
+ if (this.audio) {
328
+ this.audio.playPotActivate();
329
+ }
330
+ }
331
+ }
332
+
333
+ async startCooking(foodName) {
334
+ if (this.gameState !== 'activated') return;
335
+
336
+ this.gameState = 'cooking';
337
+ this.currentFood = foodName;
338
+ this.pot.startCooking(foodName);
339
+ this.updateUI();
340
+
341
+ this.showDebug(`开始烹饪: ${foodName}`);
342
+
343
+ // 播放烹饪音效
344
+ if (this.audio) {
345
+ this.audio.playCooking();
346
+ // 烹饪过程中定期播放冒泡声
347
+ this.cookingSoundInterval = setInterval(() => {
348
+ if (this.gameState === 'cooking') {
349
+ this.audio.playCooking();
350
+ }
351
+ }, 1000);
352
+ }
353
+
354
+ try {
355
+ // 生成食物和动物素材
356
+ await this.generateAssets(foodName);
357
+
358
+ // 烹饪完成,开始溢出
359
+ setTimeout(() => {
360
+ this.startOverflowing();
361
+ }, this.config.cookingDuration);
362
+
363
+ } catch (error) {
364
+ this.showDebug(`生成素材失败: ${error.message}`);
365
+ // 即使生成失败也继续游戏
366
+ setTimeout(() => {
367
+ this.startOverflowing();
368
+ }, this.config.cookingDuration);
369
+ }
370
+ }
371
+
372
+ async generateAssets(foodName) {
373
+ // 显示生成进度
374
+ this.updateCookingProgress(0);
375
+
376
+ // 生成食物图片
377
+ this.updateCookingProgress(30);
378
+ const foodImage = await this.textToImage.generateFood(foodName);
379
+
380
+ // 生成动物图片
381
+ this.updateCookingProgress(60);
382
+ const animalImage = await this.textToImage.generateAnimal(foodName);
383
+
384
+ // 保存生成的素材
385
+ this.pot.setFoodAssets(foodImage, animalImage);
386
+ this.updateCookingProgress(100);
387
+
388
+ this.showDebug(`素材生成完成: ${foodName}`);
389
+ }
390
+
391
+ startOverflowing() {
392
+ this.gameState = 'overflowing';
393
+ this.pot.startOverflowing();
394
+ this.foodCount = 0; // 重置计数器开始计数
395
+ this.updateUI();
396
+ this.showDebug('食物开始涌出!');
397
+
398
+ console.log(`开始溢出,食物计数重置为: ${this.foodCount}`);
399
+
400
+ // 停止烹饪音效
401
+ if (this.cookingSoundInterval) {
402
+ clearInterval(this.cookingSoundInterval);
403
+ this.cookingSoundInterval = null;
404
+ }
405
+ }
406
+
407
+ stopCooking() {
408
+ if (this.gameState === 'overflowing') {
409
+ this.gameState = 'stopped';
410
+ this.pot.stopOverflowing();
411
+ this.updateUI();
412
+ this.showDebug('停止烹饪');
413
+
414
+ // 播放停止音效
415
+ if (this.audio) {
416
+ this.audio.playStopCooking();
417
+ }
418
+
419
+ // 停止烹饪音效
420
+ if (this.cookingSoundInterval) {
421
+ clearInterval(this.cookingSoundInterval);
422
+ this.cookingSoundInterval = null;
423
+ }
424
+
425
+ // 显示统计信息
426
+ this.showGameStats();
427
+
428
+ // 5秒后重置游戏状态
429
+ setTimeout(() => {
430
+ this.resetGame();
431
+ }, 5000);
432
+ }
433
+ }
434
+
435
+ resetGame() {
436
+ this.gameState = 'idle';
437
+ this.currentFood = null;
438
+ this.foodCount = 0;
439
+ this.pot.reset();
440
+ this.particleSystem.clear();
441
+ this.animalSystem.clear();
442
+ this.updateUI();
443
+ this.showDebug('游戏已重���,可以重新开始');
444
+ }
445
+
446
+ showGameStats() {
447
+ const stats = {
448
+ food: this.currentFood,
449
+ count: this.foodCount,
450
+ animals: this.animalSystem.getAnimalCount(),
451
+ particles: this.particleSystem.getParticleCount()
452
+ };
453
+
454
+ const message = `本轮统计:\n食物:${stats.food}\n数量:${stats.count}\n动物:${stats.animals}只`;
455
+ this.showSpecialMessage(message);
456
+ this.showDebug(`游戏统计: ${JSON.stringify(stats)}`);
457
+ }
458
+
459
+ gameLoop(currentTime = 0) {
460
+ const deltaTime = currentTime - this.lastTime;
461
+ this.lastTime = currentTime;
462
+
463
+ this.update(deltaTime);
464
+ this.render();
465
+
466
+ // 性能监控和清理
467
+ this.updatePerformanceStats(currentTime);
468
+ this.performCleanupIfNeeded(currentTime);
469
+
470
+ requestAnimationFrame((time) => this.gameLoop(time));
471
+ }
472
+
473
+ update(deltaTime) {
474
+ // 更新魔法锅
475
+ this.pot.update(deltaTime);
476
+
477
+ // 更新粒子系统
478
+ this.particleSystem.update(deltaTime);
479
+
480
+ // 更新动物系统
481
+ this.animalSystem.update(deltaTime);
482
+
483
+ // 如果正在溢出,生成食物粒子(控制生成频率)
484
+ if (this.gameState === 'overflowing') {
485
+ const currentTime = Date.now();
486
+ if (currentTime - this.lastFoodSpawnTime >= this.foodSpawnInterval) {
487
+ this.spawnFoodParticles();
488
+ this.lastFoodSpawnTime = currentTime;
489
+
490
+ // 在生成食物粒子后立即检查动物生成
491
+ this.checkAnimalSpawn();
492
+ this.checkEasterEggs();
493
+ }
494
+ }
495
+ }
496
+
497
+ spawnFoodParticles() {
498
+ // 检查是否达到最大粒子数量
499
+ if (this.particleSystem.getParticleCount() >= this.config.maxParticles) {
500
+ this.showDebug('已达到最大粒子数量,停止生成新粒子');
501
+ return;
502
+ }
503
+
504
+ // 检查是否达到最大食物计数
505
+ if (this.foodCount >= this.config.maxFoodCount) {
506
+ this.showDebug('已达到最大食物数量,自动停止烹饪');
507
+ this.stopCooking();
508
+ return;
509
+ }
510
+
511
+ // 根据配置生成食物粒子
512
+ const spawnCount = Math.min(
513
+ this.config.particleSpawnRate,
514
+ this.config.maxParticles - this.particleSystem.getParticleCount(),
515
+ this.config.maxFoodCount - this.foodCount
516
+ );
517
+
518
+ for (let i = 0; i < spawnCount; i++) {
519
+ const particle = this.particleSystem.createFoodParticle(
520
+ this.pot.x,
521
+ this.pot.y - 50,
522
+ this.currentFood
523
+ );
524
+ // 注意:粒子已经在 createFoodParticle 中添加到 particleSystem.particles 了
525
+ // 这里不需要再添加到 this.particles
526
+ this.foodCount++;
527
+
528
+ console.log(`食物粒子已创建: ${this.currentFood}, 当前计数: ${this.foodCount}`);
529
+
530
+ // 随机播放食物弹出音效
531
+ if (this.audio && Math.random() < 0.3) {
532
+ this.audio.playRandomFoodPop();
533
+ }
534
+ }
535
+
536
+ // 更新UI显示
537
+ this.updateUI();
538
+ }
539
+
540
+ checkAnimalSpawn() {
541
+ // 更宽松的动物出现条件
542
+ if (this.foodCount >= this.config.animalSpawnThreshold) {
543
+ // 每2个食物就有机会出现动物,并且增加随机性
544
+ if (this.foodCount % 2 === 0 && Math.random() < 0.3) {
545
+ this.showDebug(`食物数量达到${this.foodCount},尝试生成动物`);
546
+ this.spawnAnimal();
547
+ }
548
+ } else {
549
+ // 调试信息:显示距离动物出现还需要多少食物
550
+ if (this.foodCount % 2 === 0) {
551
+ const remaining = this.config.animalSpawnThreshold - this.foodCount;
552
+ this.showDebug(`还需要${remaining}个食物才能出现动物(当前:${this.foodCount})`);
553
+ }
554
+ }
555
+ }
556
+
557
+ checkEasterEggs() {
558
+ // 草莓和西瓜的特殊彩蛋
559
+ if ((this.currentFood === 'strawberry' || this.currentFood === 'watermelon') &&
560
+ this.foodCount >= this.config.easterEggThreshold) {
561
+
562
+ // 检查是否已经有小女孩了
563
+ const hasGirl = this.animalSystem.animals.some(animal => animal.type === 'girl');
564
+
565
+ if (!hasGirl && this.foodCount % 15 === 0) {
566
+ this.spawnLittleGirl();
567
+ this.showSpecialMessage('小女孩被美味的水果吸引过来了!');
568
+ }
569
+ }
570
+
571
+ // 其他隐藏彩蛋
572
+ this.checkSecretEasterEggs();
573
+ }
574
+
575
+ checkSecretEasterEggs() {
576
+ // 特殊组合彩蛋
577
+ if (this.foodCount === 100) {
578
+ this.createCelebrationEffect();
579
+ this.showSpecialMessage('恭喜!你制作了100个食物!');
580
+ }
581
+
582
+ // 特定食物的特殊效果
583
+ if (this.currentFood === 'cake' && this.foodCount >= 20) {
584
+ this.createBirthdayEffect();
585
+ }
586
+ }
587
+
588
+ spawnAnimal() {
589
+ // 检查是否达到最大动物数量
590
+ if (this.animalSystem.getAnimalCount() >= this.config.maxAnimals) {
591
+ this.showDebug('已达到最大动物数量,不再生成新动物');
592
+ return;
593
+ }
594
+
595
+ // 检查当前食物是否已设置
596
+ if (!this.currentFood) {
597
+ this.showDebug('当前食物未设置,无法生成动物');
598
+ return;
599
+ }
600
+
601
+ const side = Math.random() < 0.5 ? 'left' : 'right';
602
+ console.log(`尝试生成动物: 食物类型=${this.currentFood}, 边=${side}, 当前动物数=${this.animalSystem.getAnimalCount()}`);
603
+
604
+ const animal = this.animalSystem.createAnimal(this.currentFood, side);
605
+ if (animal) {
606
+ // 动物已经在 createAnimal 方法中添加到 animalSystem.animals 了
607
+ this.showDebug(`🐾 小动物出现了!类型: ${animal.type}, 食物: ${this.currentFood}, 边: ${side}, 总数: ${this.animalSystem.getAnimalCount()}`);
608
+
609
+ // 播放动物出现音效
610
+ if (this.audio) {
611
+ this.audio.playAnimalAppear();
612
+ }
613
+ } else {
614
+ this.showDebug('❌ 动物生成失败');
615
+ }
616
+ }
617
+
618
+ spawnLittleGirl() {
619
+ const girl = this.animalSystem.createLittleGirl('right');
620
+ if (girl) {
621
+ // 小女孩已经在 createLittleGirl 方法中添加到 animalSystem.animals 了
622
+ this.showDebug('小女孩出现了!');
623
+
624
+ // 播放小女孩出现音效
625
+ if (this.audio) {
626
+ this.audio.playGirlAppear();
627
+ }
628
+
629
+ // 创建特殊效果
630
+ this.particleSystem.createSparkleParticles(
631
+ this.canvas.width - 100,
632
+ this.canvas.height - 100,
633
+ 10
634
+ );
635
+ }
636
+ }
637
+
638
+ createCelebrationEffect() {
639
+ // 播放庆祝音效
640
+ if (this.audio) {
641
+ this.audio.playCelebration();
642
+ }
643
+
644
+ // 创建庆祝烟花效果
645
+ for (let i = 0; i < 5; i++) {
646
+ setTimeout(() => {
647
+ const x = Math.random() * this.canvas.width;
648
+ const y = Math.random() * this.canvas.height * 0.5;
649
+ this.particleSystem.createExplosionParticles(x, y, 15, '#FFD700');
650
+ this.particleSystem.createExplosionParticles(x, y, 15, '#FF69B4');
651
+ }, i * 500);
652
+ }
653
+ }
654
+
655
+ createBirthdayEffect() {
656
+ // 生日蛋糕特效
657
+ const centerX = this.canvas.width / 2;
658
+ const centerY = this.canvas.height / 2;
659
+
660
+ this.particleSystem.createSparkleParticles(centerX, centerY, 20);
661
+ this.showSpecialMessage('生日快乐!🎂');
662
+ }
663
+
664
+ showSpecialMessage(message) {
665
+ // 显示特殊消息
666
+ const messageDiv = document.createElement('div');
667
+ messageDiv.textContent = message;
668
+ messageDiv.style.cssText = `
669
+ position: fixed;
670
+ top: 50%;
671
+ left: 50%;
672
+ transform: translate(-50%, -50%);
673
+ background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
674
+ color: white;
675
+ padding: 20px 30px;
676
+ border-radius: 15px;
677
+ font-size: 18px;
678
+ font-weight: bold;
679
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
680
+ z-index: 10000;
681
+ animation: specialMessage 3s ease-in-out forwards;
682
+ `;
683
+
684
+ document.body.appendChild(messageDiv);
685
+
686
+ setTimeout(() => {
687
+ if (messageDiv.parentNode) {
688
+ messageDiv.parentNode.removeChild(messageDiv);
689
+ }
690
+ }, 3000);
691
+ }
692
+
693
+ render() {
694
+ // 清空画布
695
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
696
+
697
+ // 绘制背景
698
+ this.renderBackground();
699
+
700
+ // 绘制魔法锅
701
+ this.pot.render(this.ctx);
702
+
703
+ // 绘制粒子
704
+ this.particleSystem.render(this.ctx);
705
+
706
+ // 绘制动物
707
+ this.animalSystem.render(this.ctx);
708
+
709
+ // 绘制特效
710
+ this.renderEffects();
711
+ }
712
+
713
+ renderBackground() {
714
+ // 绘制渐变背景
715
+ const gradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height);
716
+ gradient.addColorStop(0, '#87CEEB');
717
+ gradient.addColorStop(1, '#98FB98');
718
+
719
+ this.ctx.fillStyle = gradient;
720
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
721
+
722
+ // 绘制装饰性元素
723
+ this.renderDecorations();
724
+ }
725
+
726
+ renderDecorations() {
727
+ // 绘制云朵
728
+ this.ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
729
+ this.drawCloud(100, 80, 60);
730
+ this.drawCloud(300, 60, 40);
731
+ this.drawCloud(500, 90, 50);
732
+ }
733
+
734
+ drawCloud(x, y, size) {
735
+ this.ctx.beginPath();
736
+ this.ctx.arc(x, y, size * 0.5, 0, Math.PI * 2);
737
+ this.ctx.arc(x + size * 0.3, y, size * 0.6, 0, Math.PI * 2);
738
+ this.ctx.arc(x + size * 0.6, y, size * 0.4, 0, Math.PI * 2);
739
+ this.ctx.arc(x - size * 0.3, y, size * 0.4, 0, Math.PI * 2);
740
+ this.ctx.fill();
741
+ }
742
+
743
+ renderEffects() {
744
+ // 绘制魔法粒子效果
745
+ if (this.gameState === 'activated' || this.gameState === 'cooking') {
746
+ this.renderMagicSparkles();
747
+ }
748
+ }
749
+
750
+ renderMagicSparkles() {
751
+ const time = Date.now() * 0.005;
752
+ this.ctx.fillStyle = '#FFD700';
753
+
754
+ for (let i = 0; i < 5; i++) {
755
+ const angle = (i / 5) * Math.PI * 2 + time;
756
+ const radius = 80 + Math.sin(time + i) * 20;
757
+ const x = this.pot.x + Math.cos(angle) * radius;
758
+ const y = this.pot.y + Math.sin(angle) * radius;
759
+
760
+ this.ctx.beginPath();
761
+ this.ctx.arc(x, y, 3, 0, Math.PI * 2);
762
+ this.ctx.fill();
763
+ }
764
+ }
765
+
766
+ updateUI() {
767
+ // 更新锅状态
768
+ const potStatus = document.getElementById('potStatus');
769
+ const statusIndicator = potStatus.querySelector('.status-indicator');
770
+
771
+ statusIndicator.className = 'status-indicator';
772
+
773
+ switch (this.gameState) {
774
+ case 'idle':
775
+ statusIndicator.textContent = '魔法锅休眠中';
776
+ statusIndicator.classList.add('inactive');
777
+ break;
778
+ case 'activated':
779
+ statusIndicator.textContent = '魔法锅已激活';
780
+ statusIndicator.classList.add('active');
781
+ break;
782
+ case 'cooking':
783
+ statusIndicator.textContent = '正在烹饪中...';
784
+ statusIndicator.classList.add('cooking');
785
+ break;
786
+ case 'overflowing':
787
+ statusIndicator.textContent = '食物涌出中!';
788
+ statusIndicator.classList.add('overflowing');
789
+ break;
790
+ case 'stopped':
791
+ statusIndicator.textContent = '烹饪已停止';
792
+ statusIndicator.classList.add('inactive');
793
+ break;
794
+ }
795
+
796
+ // 更新烹饪信息
797
+ const cookingInfo = document.getElementById('cookingInfo');
798
+ const currentFoodSpan = document.getElementById('currentFood');
799
+
800
+ if (this.gameState === 'cooking' && this.currentFood) {
801
+ cookingInfo.style.display = 'block';
802
+ currentFoodSpan.textContent = this.currentFood;
803
+ } else {
804
+ cookingInfo.style.display = 'none';
805
+ }
806
+
807
+ // 更新食物计数器
808
+ const foodCounter = document.getElementById('foodCounter');
809
+ const foodCount = document.getElementById('foodCount');
810
+ const foodEmoji = document.getElementById('foodEmoji');
811
+
812
+ if (this.gameState === 'overflowing' || this.gameState === 'stopped') {
813
+ foodCounter.style.display = 'block';
814
+ foodCount.textContent = this.foodCount;
815
+ foodEmoji.textContent = this.getFoodEmoji(this.currentFood);
816
+ } else {
817
+ foodCounter.style.display = 'none';
818
+ }
819
+
820
+ // 更新语音提示
821
+ this.updateVoiceHints();
822
+
823
+ // 更新性能监控
824
+ this.updatePerformanceDisplay();
825
+ }
826
+
827
+ updateVoiceHints() {
828
+ const foodHint = document.getElementById('foodHint');
829
+ const stopHint = document.getElementById('stopHint');
830
+
831
+ foodHint.style.display = this.gameState === 'activated' ? 'block' : 'none';
832
+ stopHint.style.display = this.gameState === 'overflowing' ? 'block' : 'none';
833
+ }
834
+
835
+ updatePerformanceDisplay() {
836
+ const performanceMonitor = document.getElementById('performanceMonitor');
837
+ const particleCount = document.getElementById('particleCount');
838
+ const animalCount = document.getElementById('animalCount');
839
+ const fpsCount = document.getElementById('fpsCount');
840
+
841
+ if (performanceMonitor && particleCount && animalCount && fpsCount) {
842
+ const perfInfo = this.getPerformanceInfo();
843
+
844
+ particleCount.textContent = perfInfo.particles;
845
+ animalCount.textContent = perfInfo.animals;
846
+ fpsCount.textContent = perfInfo.fps;
847
+
848
+ // 根据游戏状态显示/隐藏性能监控
849
+ const shouldShow = this.gameState === 'overflowing' ||
850
+ this.gameState === 'cooking' ||
851
+ perfInfo.particles > 50;
852
+
853
+ performanceMonitor.style.display = shouldShow ? 'block' : 'none';
854
+
855
+ // 性���警告颜色
856
+ if (perfInfo.particles > this.config.maxParticles * 0.8) {
857
+ particleCount.style.color = '#ff6b6b';
858
+ } else {
859
+ particleCount.style.color = '#ffffff';
860
+ }
861
+
862
+ if (perfInfo.animals > this.config.maxAnimals * 0.8) {
863
+ animalCount.style.color = '#ff6b6b';
864
+ } else {
865
+ animalCount.style.color = '#ffffff';
866
+ }
867
+
868
+ if (perfInfo.fps < 30) {
869
+ fpsCount.style.color = '#ff6b6b';
870
+ } else if (perfInfo.fps < 45) {
871
+ fpsCount.style.color = '#f39c12';
872
+ } else {
873
+ fpsCount.style.color = '#27ae60';
874
+ }
875
+ }
876
+ }
877
+
878
+ updateCookingProgress(progress) {
879
+ const progressFill = document.getElementById('progressFill');
880
+ if (progressFill) {
881
+ progressFill.style.width = progress + '%';
882
+ }
883
+ }
884
+
885
+ updateVoiceStatus(status) {
886
+ const voiceStatus = document.getElementById('voiceStatus');
887
+ const statusText = voiceStatus.querySelector('.status-text');
888
+
889
+ switch (status) {
890
+ case 'listening':
891
+ statusText.textContent = '正在听取...';
892
+ voiceStatus.style.background = '#e74c3c';
893
+ break;
894
+ case 'processing':
895
+ statusText.textContent = '处理中...';
896
+ voiceStatus.style.background = '#f39c12';
897
+ break;
898
+ case 'ready':
899
+ statusText.textContent = '准备就绪';
900
+ voiceStatus.style.background = '#4ecdc4';
901
+ break;
902
+ case 'error':
903
+ statusText.textContent = '语音错误';
904
+ voiceStatus.style.background = '#95a5a6';
905
+ break;
906
+ }
907
+
908
+ // 更新语音激活按钮状态
909
+ this.updateVoiceActivateButton();
910
+ }
911
+
912
+ getFoodEmoji(foodName) {
913
+ const emojiMap = {
914
+ 'apple': '🍎',
915
+ 'banana': '🍌',
916
+ 'orange': '🍊',
917
+ 'strawberry': '🍓',
918
+ 'watermelon': '🍉',
919
+ 'grape': '🍇',
920
+ 'pizza': '🍕',
921
+ 'burger': '🍔',
922
+ 'cake': '🍰',
923
+ 'cookie': '🍪',
924
+ 'bread': '🍞',
925
+ 'cheese': '🧀'
926
+ };
927
+
928
+ return emojiMap[foodName] || '🍽️';
929
+ }
930
+
931
+ showDebug(message) {
932
+ const debugLog = document.getElementById('debugLog');
933
+ if (debugLog) {
934
+ const time = new Date().toLocaleTimeString();
935
+ const perfInfo = this.getPerformanceInfo();
936
+ const perfText = `FPS:${perfInfo.fps} P:${perfInfo.particles} A:${perfInfo.animals} M:${perfInfo.memoryUsage}KB`;
937
+ debugLog.innerHTML += `<div>[${time}] [${perfText}] ${message}</div>`;
938
+ debugLog.scrollTop = debugLog.scrollHeight;
939
+
940
+ // 限制调试日志行数
941
+ const lines = debugLog.children;
942
+ if (lines.length > 50) {
943
+ for (let i = 0; i < 10; i++) {
944
+ debugLog.removeChild(lines[0]);
945
+ }
946
+ }
947
+ }
948
+ console.log(`[MagicPot] ${message}`);
949
+ }
950
+
951
+ toggleDebug() {
952
+ const debugInfo = document.getElementById('debugInfo');
953
+ debugInfo.style.display = debugInfo.style.display === 'none' ? 'block' : 'none';
954
+ }
955
+
956
+ resizeCanvas() {
957
+ this.setupCanvas();
958
+ }
959
+
960
+ showVoiceHelp() {
961
+ const helpContent = `
962
+ <h3>🎤 语音命令帮助</h3>
963
+ <div class="help-section">
964
+ <h4>🪄 激活魔法锅</h4>
965
+ <p>说出:"<strong>cook cook cook pot</strong>"</p>
966
+ <p>其他可用命令:</p>
967
+ <ul>
968
+ <li>"magic pot cook"</li>
969
+ <li>"cook three times pot"</li>
970
+ </ul>
971
+ </div>
972
+
973
+ <div class="help-section">
974
+ <h4>🍎 开始烹饪</h4>
975
+ <p>魔法锅激活后,说出任何食物名称:</p>
976
+ <ul>
977
+ <li><strong>水果:</strong>apple, banana, orange, strawberry, watermelon, grape</li>
978
+ <li><strong>主食:</strong>pizza, burger, bread, rice, noodles</li>
979
+ <li><strong>甜点:</strong>cake, cookie</li>
980
+ <li><strong>其他:</strong>cheese, fish, chicken</li>
981
+ </ul>
982
+ </div>
983
+
984
+ <div class="help-section">
985
+ <h4>🛑 停止烹饪</h4>
986
+ <p>说出:"<strong>stop stop stop pot</strong>"</p>
987
+ <p>其他可用命令:</p>
988
+ <ul>
989
+ <li>"magic pot stop"</li>
990
+ <li>"stop three times pot"</li>
991
+ </ul>
992
+ </div>
993
+
994
+ <div class="help-section">
995
+ <h4>🎁 特殊彩蛋</h4>
996
+ <ul>
997
+ <li><strong>草莓/西瓜:</strong>数量达到30个时,小女孩会出现</li>
998
+ <li><strong>生日蛋糕:</strong>制作蛋糕时有特殊庆祝效果</li>
999
+ <li><strong>百食庆祝:</strong>制作100个食物时有烟花效果</li>
1000
+ </ul>
1001
+ </div>
1002
+
1003
+ <div class="help-section">
1004
+ <h4>🔊 音效控制</h4>
1005
+ <ul>
1006
+ <li><strong>音效按钮:</strong>点击🔊按钮开关音效</li>
1007
+ <li><strong>F10:</strong>快捷键切换音效开关</li>
1008
+ <li><strong>F11:</strong>测试所有音效</li>
1009
+ </ul>
1010
+ </div>
1011
+
1012
+ <div class="help-section">
1013
+ <h4>🎵 音效说明</h4>
1014
+ <ul>
1015
+ <li><strong>激活音效:</strong>魔法锅激活时的神奇音调</li>
1016
+ <li><strong>烹饪音效:</strong>烹饪过程中的冒泡声</li>
1017
+ <li><strong>食物弹出:</strong>食物涌出时的弹跳声</li>
1018
+ <li><strong>动物出现:</strong>小动物出现时的鸟叫声</li>
1019
+ <li><strong>小女孩:</strong>小女孩出现时的铃声</li>
1020
+ <li><strong>庆祝音效:</strong>达成成就时的号角声</li>
1021
+ </ul>
1022
+ </div>
1023
+
1024
+ <div class="help-section">
1025
+ <h4>💡 使用技巧</h4>
1026
+ <ul>
1027
+ <li>说话清晰,语速适中</li>
1028
+ <li>确保浏览器已允许麦克风权限</li>
1029
+ <li>在安静的环境中使用效果更佳</li>
1030
+ <li>按F12可查看调试信息</li>
1031
+ <li><strong>按F9激活/取消激活键盘面板</strong></li>
1032
+ <li><strong>按F8显示/隐藏键盘面板</strong></li>
1033
+ <li><strong>按F7清空命令历史</strong></li>
1034
+ <li><strong>按F5重置面板位置</strong></li>
1035
+ <li><strong>按F4调整食物生成速度</strong></li>
1036
+ <li><strong>按Alt+A强制生成动物(调试)</strong></li>
1037
+ <li>首次使用时点击任意按钮激活音频</li>
1038
+ </ul>
1039
+ </div>
1040
+
1041
+ <div class="help-section">
1042
+ <h4>⌨️ 键盘输入调试</h4>
1043
+ <ul>
1044
+ <li><strong>F9:</strong> 激活/取消激活键盘面板 ⭐</li>
1045
+ <li><strong>Ctrl+K:</strong> 聚焦到输入框(自动激活)</li>
1046
+ <li><strong>Ctrl+反引号:</strong> 切换输入面板</li>
1047
+ <li><strong>数字键1-6:</strong> 快速执行常用命令</li>
1048
+ <li><strong>↑↓方向键:</strong> 浏览命令历史</li>
1049
+ <li><strong>Enter:</strong> 执行当前输入的命令</li>
1050
+ <li><strong>Esc:</strong> 清空输入或取消激活</li>
1051
+ </ul>
1052
+ <p style="color: #f39c12; font-style: italic;">
1053
+ 💡 面板默认隐藏,按F9激活并显示面板后才能输入命令<br>
1054
+ 🖱️ 激活后可拖拽标题栏移动面板,按F5重置到右下角<br>
1055
+ 👁️ 按F8可手动显示/隐藏面板,取消激活时自动隐藏
1056
+ </p>
1057
+ </div>
1058
+ `;
1059
+
1060
+ this.showModal('语音命令帮助', helpContent);
1061
+ }
1062
+
1063
+ showModal(title, content) {
1064
+ // 创建模态框
1065
+ const modal = document.createElement('div');
1066
+ modal.className = 'help-modal';
1067
+ modal.innerHTML = `
1068
+ <div class="modal-overlay"></div>
1069
+ <div class="modal-content">
1070
+ <div class="modal-header">
1071
+ <h2>${title}</h2>
1072
+ <button class="modal-close">✕</button>
1073
+ </div>
1074
+ <div class="modal-body">
1075
+ ${content}
1076
+ </div>
1077
+ </div>
1078
+ `;
1079
+
1080
+ document.body.appendChild(modal);
1081
+
1082
+ // 关闭按钮事件
1083
+ const closeBtn = modal.querySelector('.modal-close');
1084
+ const overlay = modal.querySelector('.modal-overlay');
1085
+
1086
+ const closeModal = () => {
1087
+ modal.style.animation = 'modalFadeOut 0.3s ease-out forwards';
1088
+ setTimeout(() => {
1089
+ if (modal.parentNode) {
1090
+ modal.parentNode.removeChild(modal);
1091
+ }
1092
+ }, 300);
1093
+ };
1094
+
1095
+ closeBtn.addEventListener('click', closeModal);
1096
+ overlay.addEventListener('click', closeModal);
1097
+
1098
+ // ESC键关闭
1099
+ const handleKeydown = (e) => {
1100
+ if (e.key === 'Escape') {
1101
+ closeModal();
1102
+ document.removeEventListener('keydown', handleKeydown);
1103
+ }
1104
+ };
1105
+ document.addEventListener('keydown', handleKeydown);
1106
+ }
1107
+
1108
+ // 性能监控和优化方法
1109
+ startPerformanceMonitoring() {
1110
+ this.showDebug('性能监控已启动');
1111
+ }
1112
+
1113
+ updatePerformanceStats(currentTime) {
1114
+ // 更新FPS
1115
+ this.performanceStats.frameCount++;
1116
+ if (currentTime - this.performanceStats.lastFpsTime >= 1000) {
1117
+ this.performanceStats.fps = this.performanceStats.frameCount;
1118
+ this.performanceStats.frameCount = 0;
1119
+ this.performanceStats.lastFpsTime = currentTime;
1120
+ }
1121
+
1122
+ // 更新粒子和动物统计
1123
+ this.performanceStats.particles = this.particleSystem.getParticleCount();
1124
+ this.performanceStats.animals = this.animalSystem.getAnimalCount();
1125
+ }
1126
+
1127
+ performCleanupIfNeeded(currentTime) {
1128
+ // 定期清理
1129
+ if (currentTime - this.lastCleanupTime >= this.config.cleanupInterval) {
1130
+ this.performMemoryCleanup();
1131
+ this.lastCleanupTime = currentTime;
1132
+ }
1133
+ }
1134
+
1135
+ performMemoryCleanup() {
1136
+ const beforeParticles = this.particleSystem.getParticleCount();
1137
+ const beforeAnimals = this.animalSystem.getAnimalCount();
1138
+
1139
+ // 清理地面上过多的粒子
1140
+ this.cleanupGroundedParticles();
1141
+
1142
+ // 清理长时间存在的动物
1143
+ this.cleanupOldAnimals();
1144
+
1145
+ // 清理无效的粒子
1146
+ this.particleSystem.cleanupInvalidParticles();
1147
+
1148
+ const afterParticles = this.particleSystem.getParticleCount();
1149
+ const afterAnimals = this.animalSystem.getAnimalCount();
1150
+
1151
+ if (beforeParticles !== afterParticles || beforeAnimals !== afterAnimals) {
1152
+ this.showDebug(`内存清理完成 - 粒子: ${beforeParticles}→${afterParticles}, 动物: ${beforeAnimals}→${afterAnimals}`);
1153
+ }
1154
+ }
1155
+
1156
+ cleanupGroundedParticles() {
1157
+ const groundedParticles = this.particleSystem.particles.filter(p =>
1158
+ p.type === 'food' && p.isGrounded
1159
+ );
1160
+
1161
+ if (groundedParticles.length > this.config.maxGroundedParticles) {
1162
+ // 移除最老的地面粒子
1163
+ const toRemove = groundedParticles.length - this.config.maxGroundedParticles;
1164
+ const oldestParticles = groundedParticles
1165
+ .sort((a, b) => a.lifeTime - b.lifeTime)
1166
+ .slice(0, toRemove);
1167
+
1168
+ oldestParticles.forEach(particle => {
1169
+ const index = this.particleSystem.particles.indexOf(particle);
1170
+ if (index > -1) {
1171
+ this.particleSystem.particles.splice(index, 1);
1172
+ }
1173
+ });
1174
+ }
1175
+ }
1176
+
1177
+ cleanupOldAnimals() {
1178
+ // 移除长时间未活动的动物
1179
+ const oldAnimals = this.animalSystem.animals.filter(animal =>
1180
+ animal.lifeTime > 45 && animal.state !== 'eating'
1181
+ );
1182
+
1183
+ oldAnimals.forEach(animal => {
1184
+ animal.shouldRemove = true;
1185
+ });
1186
+ }
1187
+
1188
+ // 获取性能统计信息
1189
+ getPerformanceInfo() {
1190
+ return {
1191
+ fps: this.performanceStats.fps,
1192
+ particles: this.performanceStats.particles,
1193
+ animals: this.performanceStats.animals,
1194
+ foodCount: this.foodCount,
1195
+ gameState: this.gameState,
1196
+ memoryUsage: this.estimateMemoryUsage()
1197
+ };
1198
+ }
1199
+
1200
+ estimateMemoryUsage() {
1201
+ // 粗略估算内存使用量(KB)
1202
+ const particleMemory = this.performanceStats.particles * 0.5; // 每个粒子约0.5KB
1203
+ const animalMemory = this.performanceStats.animals * 1; // 每个动物约1KB
1204
+ const baseMemory = 100; // 基础内存约100KB
1205
+
1206
+ return Math.round(baseMemory + particleMemory + animalMemory);
1207
+ }
1208
+
1209
+ // 强制生成动物(调试用)
1210
+ forceSpawnAnimal() {
1211
+ console.log('=== 强制生成动物调试 ===');
1212
+ console.log(`当前食物: ${this.currentFood}`);
1213
+ console.log(`食物数量: ${this.foodCount}`);
1214
+ console.log(`当前动物数: ${this.animalSystem.getAnimalCount()}`);
1215
+ console.log(`最大动物数: ${this.config.maxAnimals}`);
1216
+ console.log(`游戏状态: ${this.gameState}`);
1217
+ console.log(`Canvas尺寸: ${this.canvas.width}x${this.canvas.height}`);
1218
+
1219
+ if (!this.currentFood) {
1220
+ this.showDebug('❌ 当前没有食物类型,无法生成动物');
1221
+ return;
1222
+ }
1223
+
1224
+ if (this.animalSystem.getAnimalCount() >= this.config.maxAnimals) {
1225
+ this.showDebug(`❌ 已达到最大动物数量 ${this.config.maxAnimals},清理一些动物后再试`);
1226
+ // 清理一些老动物
1227
+ this.animalSystem.makeAllAnimalsLeave();
1228
+ return;
1229
+ }
1230
+
1231
+ this.showDebug('🔧 强制尝试生成动物...');
1232
+ this.spawnAnimal();
1233
+
1234
+ // 显示当前所有动物的状态
1235
+ setTimeout(() => {
1236
+ console.log('=== 当前动物状态 ===');
1237
+ this.animalSystem.animals.forEach((animal, index) => {
1238
+ console.log(`动物${index + 1}: ${animal.type}, 位置(${animal.x}, ${animal.y}), 状态: ${animal.state}, emoji: ${animal.emoji}`);
1239
+ });
1240
+ }, 100);
1241
+ }
1242
+
1243
+ // 音效控制方法
1244
+ toggleAudio() {
1245
+ if (this.audio) {
1246
+ const newState = !this.audio.isEnabled;
1247
+ this.audio.setEnabled(newState);
1248
+ this.updateAudioButton();
1249
+
1250
+ if (newState) {
1251
+ this.showDebug('音效已开启');
1252
+ // 播放测试音效
1253
+ setTimeout(() => {
1254
+ this.audio.playPotActivate();
1255
+ }, 100);
1256
+ } else {
1257
+ this.showDebug('音效已关闭');
1258
+ }
1259
+ }
1260
+ }
1261
+
1262
+ updateAudioButton() {
1263
+ const audioButton = document.getElementById('audioButton');
1264
+ if (audioButton && this.audio) {
1265
+ if (this.audio.isEnabled) {
1266
+ audioButton.textContent = '🔊';
1267
+ audioButton.classList.remove('muted');
1268
+ audioButton.title = '点击关闭音效';
1269
+ } else {
1270
+ audioButton.textContent = '🔇';
1271
+ audioButton.classList.add('muted');
1272
+ audioButton.title = '点击开启音效';
1273
+ }
1274
+ }
1275
+ }
1276
+
1277
+ updateVoiceActivateButton() {
1278
+ const voiceActivateButton = document.getElementById('voiceActivateButton');
1279
+ if (voiceActivateButton && this.voice) {
1280
+ const status = this.voice.getStatus();
1281
+
1282
+ // 移除所有状态类
1283
+ voiceActivateButton.classList.remove('active', 'disabled');
1284
+
1285
+ if (!status.isSupported) {
1286
+ voiceActivateButton.classList.add('disabled');
1287
+ voiceActivateButton.title = '浏览器不支持语音识别';
1288
+ voiceActivateButton.textContent = '❌';
1289
+ } else if (status.isDisabled) {
1290
+ voiceActivateButton.classList.add('disabled');
1291
+ voiceActivateButton.title = '语音识别已禁用,点击重新激活';
1292
+ voiceActivateButton.textContent = '🔇';
1293
+ } else if (status.isListening) {
1294
+ voiceActivateButton.classList.add('active');
1295
+ voiceActivateButton.title = '语音识别运行中';
1296
+ voiceActivateButton.textContent = '🎤';
1297
+ } else {
1298
+ voiceActivateButton.title = '点击激活语音识别';
1299
+ voiceActivateButton.textContent = '🎤';
1300
+ }
1301
+ }
1302
+ }
1303
+
1304
+ // 音效测试功能
1305
+ testAudio() {
1306
+ if (this.audio) {
1307
+ this.showDebug('开始音效测试...');
1308
+ this.audio.testAllSounds();
1309
+ }
1310
+ }
1311
+
1312
+ // 重启语音识别
1313
+ restartVoice() {
1314
+ if (this.voice) {
1315
+ this.showDebug('手动重启语音识别系统');
1316
+ this.voice.restart();
1317
+ }
1318
+ }
1319
+
1320
+ // 手动激活语音识别
1321
+ manualActivateVoice() {
1322
+ if (this.voice) {
1323
+ this.showDebug('手动激活语音识别');
1324
+ this.voice.manualActivate();
1325
+ }
1326
+ }
1327
+
1328
+ // 切换键盘输入面板
1329
+ toggleKeyboardPanel() {
1330
+ if (this.keyboard) {
1331
+ this.keyboard.togglePanel();
1332
+ const stats = this.keyboard.getStats();
1333
+ this.showDebug(`键盘面板${stats.isCollapsed ? '已折叠' : '已展开'}`);
1334
+ }
1335
+ }
1336
+
1337
+ // 清空命令历史
1338
+ clearCommandHistory() {
1339
+ if (this.keyboard) {
1340
+ this.keyboard.clearHistory();
1341
+ this.showDebug('键盘命令历史已清空');
1342
+ }
1343
+ }
1344
+
1345
+ // 切换键盘面板激活状态
1346
+ toggleKeyboardActivation() {
1347
+ if (this.keyboard) {
1348
+ console.log('F9 pressed - toggling keyboard activation');
1349
+ this.keyboard.toggleActiveState();
1350
+ const stats = this.keyboard.getStats();
1351
+
1352
+ // 激活时自动展开面板,取消激活时隐藏面板
1353
+ if (stats.isActive) {
1354
+ this.keyboard.setPanelCollapsed(false);
1355
+ this.showDebug('键盘面板已激活并显示');
1356
+ } else {
1357
+ this.keyboard.setPanelCollapsed(true);
1358
+ this.showDebug('键盘面板已取消激活并隐藏');
1359
+ }
1360
+
1361
+ console.log('Keyboard activation state:', stats.isActive);
1362
+ } else {
1363
+ console.log('Keyboard handler not initialized');
1364
+ }
1365
+ }
1366
+
1367
+ // 重置键盘面板位置
1368
+ resetKeyboardPanelPosition() {
1369
+ if (this.keyboard) {
1370
+ this.keyboard.resetPosition();
1371
+ this.showDebug('键盘面板位置已重置');
1372
+ }
1373
+ }
1374
+
1375
+ // 调整食物生成速度
1376
+ adjustFoodSpawnSpeed() {
1377
+ // 循环切换生成速度:慢速(800ms) -> 中速(400ms) -> 快速(200ms) -> 超快(100ms)
1378
+ const speeds = [
1379
+ { interval: 800, rate: 1, name: '慢速' },
1380
+ { interval: 400, rate: 1, name: '中速' },
1381
+ { interval: 200, rate: 2, name: '快速' },
1382
+ { interval: 100, rate: 3, name: '超快' }
1383
+ ];
1384
+
1385
+ // 找到当前速度的索引
1386
+ let currentIndex = speeds.findIndex(speed => speed.interval === this.foodSpawnInterval);
1387
+ if (currentIndex === -1) currentIndex = 0; // 默认慢速
1388
+
1389
+ // 切换到下一个速度
1390
+ const nextIndex = (currentIndex + 1) % speeds.length;
1391
+ const newSpeed = speeds[nextIndex];
1392
+
1393
+ this.foodSpawnInterval = newSpeed.interval;
1394
+ this.config.particleSpawnRate = newSpeed.rate;
1395
+
1396
+ this.showDebug(`食物生成速度已调整为: ${newSpeed.name} (${newSpeed.interval}ms间隔)`);
1397
+ this.showSpecialMessage(`🍽️ 食物生成速度: ${newSpeed.name}`);
1398
+ }
1399
+
1400
+ // 获取完整的调试信息
1401
+ getDebugInfo() {
1402
+ const info = {
1403
+ game: this.getPerformanceInfo(),
1404
+ voice: this.voice ? this.voice.getStatus() : null,
1405
+ keyboard: this.keyboard ? this.keyboard.getStats() : null
1406
+ };
1407
+
1408
+ return info;
1409
+ }
1410
+ }
1411
+
1412
+ // 游戏初始化
1413
+ document.addEventListener('DOMContentLoaded', () => {
1414
+ window.game = new MagicPotGame();
1415
+ });
style.css ADDED
@@ -0,0 +1,1164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 全局样式 */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ body {
9
+ font-family: 'Comic Neue', cursive;
10
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
11
+ min-height: 100vh;
12
+ overflow: hidden;
13
+ }
14
+
15
+ /* 游戏容器 */
16
+ .game-container {
17
+ width: 100vw;
18
+ height: 100vh;
19
+ display: flex;
20
+ flex-direction: column;
21
+ position: relative;
22
+ }
23
+
24
+ /* 游戏标题区域 */
25
+ .game-header {
26
+ background: rgba(255, 255, 255, 0.95);
27
+ padding: 15px 20px;
28
+ display: flex;
29
+ justify-content: space-between;
30
+ align-items: center;
31
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
32
+ border-bottom: 3px solid #ff6b6b;
33
+ }
34
+
35
+ .header-controls {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 15px;
39
+ }
40
+
41
+ .audio-button, .voice-activate-button, .help-button {
42
+ background: #ff6b6b;
43
+ color: white;
44
+ border: none;
45
+ border-radius: 50%;
46
+ width: 40px;
47
+ height: 40px;
48
+ font-size: 18px;
49
+ cursor: pointer;
50
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
51
+ transition: all 0.3s ease;
52
+ font-family: 'Comic Neue', cursive;
53
+ font-weight: bold;
54
+ }
55
+
56
+ .audio-button:hover, .voice-activate-button:hover, .help-button:hover {
57
+ background: #e55555;
58
+ transform: scale(1.1);
59
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
60
+ }
61
+
62
+ .audio-button:active, .voice-activate-button:active, .help-button:active {
63
+ transform: scale(0.95);
64
+ }
65
+
66
+ /* 语音激活按钮特殊样式 */
67
+ .voice-activate-button {
68
+ background: #4ecdc4;
69
+ position: relative;
70
+ overflow: hidden;
71
+ }
72
+
73
+ .voice-activate-button:hover {
74
+ background: #44a08d;
75
+ }
76
+
77
+ .voice-activate-button.active {
78
+ background: #27ae60;
79
+ animation: voice-active-pulse 2s infinite;
80
+ }
81
+
82
+ .voice-activate-button.disabled {
83
+ background: #95a5a6;
84
+ opacity: 0.6;
85
+ cursor: not-allowed;
86
+ }
87
+
88
+ .voice-activate-button.disabled:hover {
89
+ background: #95a5a6;
90
+ transform: none;
91
+ }
92
+
93
+ @keyframes voice-active-pulse {
94
+ 0% {
95
+ box-shadow: 0 2px 8px rgba(39, 174, 96, 0.3);
96
+ }
97
+ 50% {
98
+ box-shadow: 0 2px 8px rgba(39, 174, 96, 0.6), 0 0 0 10px rgba(39, 174, 96, 0);
99
+ }
100
+ 100% {
101
+ box-shadow: 0 2px 8px rgba(39, 174, 96, 0.3);
102
+ }
103
+ }
104
+
105
+ .audio-button.muted {
106
+ background: #95a5a6;
107
+ color: #bdc3c7;
108
+ }
109
+
110
+ .audio-button.muted:hover {
111
+ background: #7f8c8d;
112
+ }
113
+
114
+ .game-title {
115
+ font-family: 'Fredoka One', cursive;
116
+ font-size: 2.2em;
117
+ color: #ff6b6b;
118
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
119
+ margin: 0;
120
+ }
121
+
122
+ .voice-status {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 10px;
126
+ background: #4ecdc4;
127
+ padding: 8px 15px;
128
+ border-radius: 25px;
129
+ color: white;
130
+ font-weight: bold;
131
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
132
+ }
133
+
134
+ .mic-icon {
135
+ font-size: 1.2em;
136
+ animation: pulse 2s infinite;
137
+ }
138
+
139
+ @keyframes pulse {
140
+ 0%, 100% { transform: scale(1); }
141
+ 50% { transform: scale(1.1); }
142
+ }
143
+
144
+ /* 主游戏区域 */
145
+ .game-main {
146
+ flex: 1;
147
+ position: relative;
148
+ display: flex;
149
+ justify-content: center;
150
+ align-items: center;
151
+ }
152
+
153
+ #gameCanvas {
154
+ border: 4px solid #fff;
155
+ border-radius: 20px;
156
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
157
+ background: linear-gradient(45deg, #ffeaa7, #fab1a0);
158
+ }
159
+
160
+ /* 游戏UI覆盖层 */
161
+ .game-ui {
162
+ position: absolute;
163
+ top: 0;
164
+ left: 0;
165
+ width: 100%;
166
+ height: 100%;
167
+ pointer-events: none;
168
+ z-index: 10;
169
+ }
170
+
171
+ /* 键盘面板在game-ui内,需要特殊处理 */
172
+ .game-ui .keyboard-input-panel {
173
+ pointer-events: auto;
174
+ }
175
+
176
+ .game-ui .keyboard-input-panel.inactive {
177
+ pointer-events: none;
178
+ }
179
+
180
+ .game-ui .keyboard-input-panel.active {
181
+ pointer-events: auto !important;
182
+ }
183
+
184
+ /* 确保激活状态下所有子元素都可以交互 */
185
+ .keyboard-input-panel.active,
186
+ .keyboard-input-panel.active *,
187
+ .keyboard-input-panel.active .input-content,
188
+ .keyboard-input-panel.active .command-input,
189
+ .keyboard-input-panel.active .send-btn,
190
+ .keyboard-input-panel.active .quick-cmd,
191
+ .keyboard-input-panel.active .toggle-panel-btn,
192
+ .keyboard-input-panel.active .history-item {
193
+ pointer-events: auto !important;
194
+ }
195
+
196
+ /* 魔法锅状态 */
197
+ .pot-status {
198
+ position: absolute;
199
+ top: 20px;
200
+ left: 50%;
201
+ transform: translateX(-50%);
202
+ pointer-events: auto;
203
+ }
204
+
205
+ .status-indicator {
206
+ padding: 10px 20px;
207
+ border-radius: 25px;
208
+ font-weight: bold;
209
+ text-align: center;
210
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
211
+ transition: all 0.3s ease;
212
+ }
213
+
214
+ .status-indicator.inactive {
215
+ background: #95a5a6;
216
+ color: white;
217
+ }
218
+
219
+ .status-indicator.active {
220
+ background: #e74c3c;
221
+ color: white;
222
+ animation: glow 2s infinite alternate;
223
+ }
224
+
225
+ .status-indicator.cooking {
226
+ background: #f39c12;
227
+ color: white;
228
+ animation: cooking-pulse 1s infinite;
229
+ }
230
+
231
+ .status-indicator.overflowing {
232
+ background: #27ae60;
233
+ color: white;
234
+ animation: overflow-shake 0.5s infinite;
235
+ }
236
+
237
+ @keyframes glow {
238
+ from { box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3); }
239
+ to { box-shadow: 0 4px 25px rgba(231, 76, 60, 0.8); }
240
+ }
241
+
242
+ @keyframes cooking-pulse {
243
+ 0%, 100% { transform: scale(1); }
244
+ 50% { transform: scale(1.05); }
245
+ }
246
+
247
+ @keyframes overflow-shake {
248
+ 0%, 100% { transform: translateX(0); }
249
+ 25% { transform: translateX(-2px); }
250
+ 75% { transform: translateX(2px); }
251
+ }
252
+
253
+ /* 烹饪信息 */
254
+ .cooking-info {
255
+ position: absolute;
256
+ top: 80px;
257
+ left: 50%;
258
+ transform: translateX(-50%);
259
+ background: rgba(255, 255, 255, 0.95);
260
+ padding: 15px 25px;
261
+ border-radius: 15px;
262
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
263
+ text-align: center;
264
+ min-width: 200px;
265
+ }
266
+
267
+ .cooking-food {
268
+ font-size: 1.2em;
269
+ font-weight: bold;
270
+ color: #2c3e50;
271
+ margin-bottom: 10px;
272
+ }
273
+
274
+ .progress-bar {
275
+ width: 100%;
276
+ height: 8px;
277
+ background: #ecf0f1;
278
+ border-radius: 4px;
279
+ overflow: hidden;
280
+ }
281
+
282
+ .progress-fill {
283
+ height: 100%;
284
+ background: linear-gradient(90deg, #ff6b6b, #4ecdc4);
285
+ border-radius: 4px;
286
+ transition: width 0.3s ease;
287
+ width: 0%;
288
+ }
289
+
290
+ /* 食物计数器 */
291
+ .food-counter {
292
+ position: absolute;
293
+ top: 20px;
294
+ right: 20px;
295
+ background: rgba(255, 255, 255, 0.95);
296
+ padding: 15px 20px;
297
+ border-radius: 15px;
298
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
299
+ display: flex;
300
+ align-items: center;
301
+ gap: 10px;
302
+ }
303
+
304
+ /* 性能监控 */
305
+ .performance-monitor {
306
+ position: absolute;
307
+ top: 20px;
308
+ left: 20px;
309
+ background: rgba(0, 0, 0, 0.8);
310
+ color: white;
311
+ padding: 10px 15px;
312
+ border-radius: 10px;
313
+ font-family: 'Courier New', monospace;
314
+ font-size: 0.9em;
315
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
316
+ min-width: 120px;
317
+ }
318
+
319
+ .perf-item {
320
+ display: flex;
321
+ justify-content: space-between;
322
+ margin-bottom: 5px;
323
+ align-items: center;
324
+ }
325
+
326
+ .perf-item:last-child {
327
+ margin-bottom: 0;
328
+ }
329
+
330
+ .perf-label {
331
+ color: #4ecdc4;
332
+ font-weight: bold;
333
+ }
334
+
335
+ .perf-value {
336
+ color: #ffffff;
337
+ font-weight: bold;
338
+ text-align: right;
339
+ min-width: 30px;
340
+ }
341
+
342
+ .food-emoji {
343
+ font-size: 2em;
344
+ }
345
+
346
+ .count {
347
+ font-size: 1.5em;
348
+ font-weight: bold;
349
+ color: #2c3e50;
350
+ min-width: 30px;
351
+ text-align: center;
352
+ }
353
+
354
+ /* 语音提示 */
355
+ .voice-hints {
356
+ position: absolute;
357
+ bottom: 20px;
358
+ left: 50%;
359
+ transform: translateX(-50%);
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: 10px;
363
+ align-items: center;
364
+ }
365
+
366
+ .hint-item {
367
+ background: rgba(0, 0, 0, 0.8);
368
+ color: white;
369
+ padding: 8px 15px;
370
+ border-radius: 20px;
371
+ display: flex;
372
+ align-items: center;
373
+ gap: 10px;
374
+ font-size: 0.9em;
375
+ animation: hint-fade-in 0.5s ease;
376
+ }
377
+
378
+ .hint-command {
379
+ background: #ff6b6b;
380
+ padding: 4px 8px;
381
+ border-radius: 10px;
382
+ font-weight: bold;
383
+ font-size: 0.8em;
384
+ }
385
+
386
+ .hint-desc {
387
+ opacity: 0.9;
388
+ }
389
+
390
+ @keyframes hint-fade-in {
391
+ from { opacity: 0; transform: translateY(10px); }
392
+ to { opacity: 1; transform: translateY(0); }
393
+ }
394
+
395
+ /* 调试信息 */
396
+ .debug-info {
397
+ position: absolute;
398
+ top: 10px;
399
+ left: 10px;
400
+ background: rgba(0, 0, 0, 0.8);
401
+ color: white;
402
+ padding: 10px;
403
+ border-radius: 5px;
404
+ font-family: monospace;
405
+ font-size: 0.8em;
406
+ max-width: 300px;
407
+ max-height: 200px;
408
+ overflow-y: auto;
409
+ z-index: 1000;
410
+ }
411
+
412
+ /* 响应式设计 */
413
+ @media (max-width: 768px) {
414
+ .game-title {
415
+ font-size: 1.5em;
416
+ }
417
+
418
+ #gameCanvas {
419
+ width: 95vw;
420
+ height: 70vh;
421
+ }
422
+
423
+ .voice-hints {
424
+ bottom: 10px;
425
+ }
426
+
427
+ .hint-item {
428
+ font-size: 0.8em;
429
+ padding: 6px 12px;
430
+ }
431
+ }
432
+
433
+ /* 动画效果 */
434
+ .bounce-in {
435
+ animation: bounceIn 0.6s ease;
436
+ }
437
+
438
+ @keyframes bounceIn {
439
+ 0% { transform: scale(0.3); opacity: 0; }
440
+ 50% { transform: scale(1.05); }
441
+ 70% { transform: scale(0.9); }
442
+ 100% { transform: scale(1); opacity: 1; }
443
+ }
444
+
445
+ .fade-out {
446
+ animation: fadeOut 0.5s ease forwards;
447
+ }
448
+
449
+ @keyframes fadeOut {
450
+ from { opacity: 1; }
451
+ to { opacity: 0; }
452
+ }
453
+
454
+ /* 特殊效果 */
455
+ .sparkle {
456
+ position: absolute;
457
+ width: 4px;
458
+ height: 4px;
459
+ background: #ffd700;
460
+ border-radius: 50%;
461
+ animation: sparkle 1s ease-in-out infinite;
462
+ }
463
+
464
+ @keyframes sparkle {
465
+ 0%, 100% { opacity: 0; transform: scale(0); }
466
+ 50% { opacity: 1; transform: scale(1); }
467
+ }
468
+
469
+ /* 音频激活提示 */
470
+ .audio-activation-hint {
471
+ position: absolute;
472
+ top: 30%;
473
+ left: 50%;
474
+ transform: translate(-50%, -50%);
475
+ z-index: 200;
476
+ pointer-events: none;
477
+ animation: audioHintPulse 2s infinite;
478
+ }
479
+
480
+ .hint-bubble {
481
+ background: rgba(255, 107, 107, 0.95);
482
+ border: 3px solid #e74c3c;
483
+ border-radius: 20px;
484
+ padding: 15px 25px;
485
+ display: flex;
486
+ align-items: center;
487
+ gap: 10px;
488
+ box-shadow: 0 8px 32px rgba(231, 76, 60, 0.3);
489
+ color: white;
490
+ font-family: 'Comic Neue', cursive;
491
+ font-weight: bold;
492
+ }
493
+
494
+ .hint-icon {
495
+ font-size: 1.5em;
496
+ animation: audioIconBounce 1s infinite;
497
+ }
498
+
499
+ .hint-text {
500
+ font-size: 1.1em;
501
+ }
502
+
503
+ @keyframes audioHintPulse {
504
+ 0%, 100% {
505
+ transform: translate(-50%, -50%) scale(1);
506
+ opacity: 0.9;
507
+ }
508
+ 50% {
509
+ transform: translate(-50%, -50%) scale(1.05);
510
+ opacity: 1;
511
+ }
512
+ }
513
+
514
+ @keyframes audioIconBounce {
515
+ 0%, 100% {
516
+ transform: translateY(0);
517
+ }
518
+ 50% {
519
+ transform: translateY(-3px);
520
+ }
521
+ }
522
+
523
+ /* 语音输入显示 */
524
+ .voice-input-display {
525
+ position: absolute;
526
+ top: 50%;
527
+ left: 50%;
528
+ transform: translate(-50%, -50%);
529
+ z-index: 100;
530
+ pointer-events: none;
531
+ }
532
+
533
+ .speech-bubble {
534
+ background: rgba(255, 255, 255, 0.95);
535
+ border: 3px solid #ff6b6b;
536
+ border-radius: 25px;
537
+ padding: 20px 30px;
538
+ position: relative;
539
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
540
+ animation: speechBubble 0.3s ease-out;
541
+ max-width: 400px;
542
+ text-align: center;
543
+ }
544
+
545
+ .speech-bubble::before {
546
+ content: '';
547
+ position: absolute;
548
+ bottom: -15px;
549
+ left: 50%;
550
+ transform: translateX(-50%);
551
+ width: 0;
552
+ height: 0;
553
+ border-left: 15px solid transparent;
554
+ border-right: 15px solid transparent;
555
+ border-top: 15px solid #ff6b6b;
556
+ }
557
+
558
+ .speech-bubble::after {
559
+ content: '';
560
+ position: absolute;
561
+ bottom: -12px;
562
+ left: 50%;
563
+ transform: translateX(-50%);
564
+ width: 0;
565
+ height: 0;
566
+ border-left: 12px solid transparent;
567
+ border-right: 12px solid transparent;
568
+ border-top: 12px solid rgba(255, 255, 255, 0.95);
569
+ }
570
+
571
+ .speech-text {
572
+ font-size: 1.4em;
573
+ font-weight: bold;
574
+ color: #2c3e50;
575
+ margin-bottom: 8px;
576
+ font-family: 'Comic Neue', cursive;
577
+ line-height: 1.3;
578
+ }
579
+
580
+ .speech-confidence {
581
+ font-size: 0.9em;
582
+ color: #7f8c8d;
583
+ font-style: italic;
584
+ }
585
+
586
+ @keyframes speechBubble {
587
+ 0% {
588
+ opacity: 0;
589
+ transform: scale(0.3) translateY(20px);
590
+ }
591
+ 50% {
592
+ transform: scale(1.1) translateY(-5px);
593
+ }
594
+ 100% {
595
+ opacity: 1;
596
+ transform: scale(1) translateY(0);
597
+ }
598
+ }
599
+
600
+ /* 语音识别状态动画 */
601
+ .speech-bubble.listening {
602
+ border-color: #e74c3c;
603
+ animation: listening-pulse 1.5s infinite;
604
+ }
605
+
606
+ .speech-bubble.processing {
607
+ border-color: #f39c12;
608
+ animation: processing-spin 2s infinite linear;
609
+ }
610
+
611
+ .speech-bubble.recognized {
612
+ border-color: #27ae60;
613
+ animation: recognized-bounce 0.6s ease-out;
614
+ }
615
+
616
+ @keyframes listening-pulse {
617
+ 0%, 100% {
618
+ border-color: #e74c3c;
619
+ box-shadow: 0 8px 32px rgba(231, 76, 60, 0.2);
620
+ }
621
+ 50% {
622
+ border-color: #c0392b;
623
+ box-shadow: 0 8px 32px rgba(231, 76, 60, 0.4);
624
+ }
625
+ }
626
+
627
+ @keyframes processing-spin {
628
+ 0% {
629
+ border-color: #f39c12;
630
+ transform: rotate(0deg);
631
+ }
632
+ 100% {
633
+ border-color: #e67e22;
634
+ transform: rotate(360deg);
635
+ }
636
+ }
637
+
638
+ @keyframes recognized-bounce {
639
+ 0% { transform: scale(1); }
640
+ 30% { transform: scale(1.1); }
641
+ 60% { transform: scale(0.95); }
642
+ 100% { transform: scale(1); }
643
+ }
644
+
645
+ /* 帮助模态框 */
646
+ .help-modal {
647
+ position: fixed;
648
+ top: 0;
649
+ left: 0;
650
+ width: 100%;
651
+ height: 100%;
652
+ z-index: 10000;
653
+ animation: modalFadeIn 0.3s ease-out;
654
+ }
655
+
656
+ .modal-overlay {
657
+ position: absolute;
658
+ top: 0;
659
+ left: 0;
660
+ width: 100%;
661
+ height: 100%;
662
+ background: rgba(0, 0, 0, 0.5);
663
+ backdrop-filter: blur(5px);
664
+ }
665
+
666
+ .modal-content {
667
+ position: absolute;
668
+ top: 50%;
669
+ left: 50%;
670
+ transform: translate(-50%, -50%);
671
+ background: white;
672
+ border-radius: 20px;
673
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
674
+ max-width: 600px;
675
+ max-height: 80vh;
676
+ overflow: hidden;
677
+ font-family: 'Comic Neue', cursive;
678
+ }
679
+
680
+ .modal-header {
681
+ background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
682
+ color: white;
683
+ padding: 20px 30px;
684
+ display: flex;
685
+ justify-content: space-between;
686
+ align-items: center;
687
+ }
688
+
689
+ .modal-header h2 {
690
+ margin: 0;
691
+ font-family: 'Fredoka One', cursive;
692
+ font-size: 1.5em;
693
+ }
694
+
695
+ .modal-close {
696
+ background: none;
697
+ border: none;
698
+ color: white;
699
+ font-size: 24px;
700
+ cursor: pointer;
701
+ padding: 5px;
702
+ border-radius: 50%;
703
+ width: 35px;
704
+ height: 35px;
705
+ display: flex;
706
+ align-items: center;
707
+ justify-content: center;
708
+ transition: background 0.3s ease;
709
+ }
710
+
711
+ .modal-close:hover {
712
+ background: rgba(255, 255, 255, 0.2);
713
+ }
714
+
715
+ .modal-body {
716
+ padding: 30px;
717
+ overflow-y: auto;
718
+ max-height: calc(80vh - 100px);
719
+ }
720
+
721
+ .help-section {
722
+ margin-bottom: 25px;
723
+ padding-bottom: 20px;
724
+ border-bottom: 2px solid #f0f0f0;
725
+ }
726
+
727
+ .help-section:last-child {
728
+ border-bottom: none;
729
+ margin-bottom: 0;
730
+ }
731
+
732
+ .help-section h4 {
733
+ color: #ff6b6b;
734
+ margin-bottom: 10px;
735
+ font-size: 1.2em;
736
+ }
737
+
738
+ .help-section p {
739
+ margin-bottom: 10px;
740
+ line-height: 1.5;
741
+ color: #2c3e50;
742
+ }
743
+
744
+ .help-section ul {
745
+ margin: 10px 0;
746
+ padding-left: 20px;
747
+ }
748
+
749
+ .help-section li {
750
+ margin-bottom: 5px;
751
+ color: #34495e;
752
+ line-height: 1.4;
753
+ }
754
+
755
+ .help-section strong {
756
+ color: #e74c3c;
757
+ }
758
+
759
+ @keyframes modalFadeIn {
760
+ 0% {
761
+ opacity: 0;
762
+ transform: scale(0.9);
763
+ }
764
+ 100% {
765
+ opacity: 1;
766
+ transform: scale(1);
767
+ }
768
+ }
769
+
770
+ @keyframes modalFadeOut {
771
+ 0% {
772
+ opacity: 1;
773
+ transform: scale(1);
774
+ }
775
+ 100% {
776
+ opacity: 0;
777
+ transform: scale(0.9);
778
+ }
779
+ }
780
+
781
+ /* 特殊消息动画 */
782
+ @keyframes specialMessage {
783
+ 0% {
784
+ opacity: 0;
785
+ transform: translate(-50%, -50%) scale(0.3);
786
+ }
787
+ 20% {
788
+ opacity: 1;
789
+ transform: translate(-50%, -50%) scale(1.1);
790
+ }
791
+ 30% {
792
+ transform: translate(-50%, -50%) scale(1);
793
+ }
794
+ 70% {
795
+ opacity: 1;
796
+ transform: translate(-50%, -50%) scale(1);
797
+ }
798
+ 100% {
799
+ opacity: 0;
800
+ transform: translate(-50%, -50%) scale(0.8);
801
+ }
802
+ }
803
+
804
+ /* 键盘输入调试面板 */
805
+ .keyboard-input-panel {
806
+ position: fixed;
807
+ bottom: 20px;
808
+ right: 20px;
809
+ background: rgba(44, 62, 80, 0.95);
810
+ border: 2px solid #4ecdc4;
811
+ border-radius: 15px;
812
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
813
+ backdrop-filter: blur(10px);
814
+ color: white;
815
+ font-family: 'Comic Neue', cursive;
816
+ min-width: 300px;
817
+ max-width: 400px;
818
+ transition: all 0.4s ease;
819
+ z-index: 50;
820
+ pointer-events: auto;
821
+ cursor: move;
822
+ user-select: none;
823
+ }
824
+
825
+ /* 面板激活状态 */
826
+ .keyboard-input-panel.active {
827
+ z-index: 10000 !important;
828
+ transform: scale(1.02);
829
+ box-shadow: 0 12px 40px rgba(78, 205, 196, 0.4);
830
+ border-color: #ff6b6b;
831
+ background: rgba(44, 62, 80, 0.98);
832
+ pointer-events: auto !important;
833
+ }
834
+
835
+ /* 拖拽状态 */
836
+ .keyboard-input-panel.dragging {
837
+ transition: none !important;
838
+ transform: scale(1.05) rotate(2deg);
839
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
840
+ z-index: 15000 !important;
841
+ }
842
+
843
+ .keyboard-input-panel.dragging .input-content {
844
+ pointer-events: none;
845
+ }
846
+
847
+ .keyboard-input-panel.dragging .input-header {
848
+ background: linear-gradient(45deg, #ff6b6b, #e74c3c);
849
+ }
850
+
851
+ /* 面板非激活状态(灰色透明) */
852
+ .keyboard-input-panel.inactive {
853
+ opacity: 0.4;
854
+ background: rgba(108, 117, 125, 0.7);
855
+ border-color: #6c757d;
856
+ transform: scale(0.95);
857
+ pointer-events: none;
858
+ }
859
+
860
+ .keyboard-input-panel.inactive .input-header {
861
+ background: linear-gradient(45deg, #6c757d, #495057);
862
+ }
863
+
864
+ .keyboard-input-panel.inactive .command-input {
865
+ border-color: #6c757d;
866
+ background: rgba(108, 117, 125, 0.3);
867
+ color: rgba(255, 255, 255, 0.6);
868
+ }
869
+
870
+ .keyboard-input-panel.inactive .send-btn {
871
+ background: linear-gradient(45deg, #6c757d, #495057);
872
+ opacity: 0.6;
873
+ }
874
+
875
+ .keyboard-input-panel.inactive .quick-cmd {
876
+ border-color: #6c757d;
877
+ background: rgba(108, 117, 125, 0.2);
878
+ color: rgba(255, 255, 255, 0.6);
879
+ }
880
+
881
+ .keyboard-input-panel:hover:not(.inactive) {
882
+ transform: translateY(-5px);
883
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
884
+ }
885
+
886
+ .keyboard-input-panel.active:hover {
887
+ transform: scale(1.02) translateY(-5px);
888
+ box-shadow: 0 16px 50px rgba(78, 205, 196, 0.5);
889
+ }
890
+
891
+ .input-header {
892
+ background: linear-gradient(45deg, #4ecdc4, #44a08d);
893
+ padding: 15px 20px;
894
+ border-radius: 13px 13px 0 0;
895
+ display: flex;
896
+ justify-content: space-between;
897
+ align-items: center;
898
+ border-bottom: 2px solid rgba(255, 255, 255, 0.1);
899
+ cursor: move;
900
+ user-select: none;
901
+ }
902
+
903
+ .input-header h4 {
904
+ margin: 0;
905
+ font-size: 1.1em;
906
+ font-weight: bold;
907
+ color: white;
908
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
909
+ }
910
+
911
+ .toggle-panel-btn {
912
+ background: rgba(255, 255, 255, 0.2);
913
+ border: none;
914
+ border-radius: 50%;
915
+ width: 35px;
916
+ height: 35px;
917
+ color: white;
918
+ font-size: 16px;
919
+ cursor: pointer;
920
+ transition: all 0.3s ease;
921
+ display: flex;
922
+ align-items: center;
923
+ justify-content: center;
924
+ }
925
+
926
+ .toggle-panel-btn:hover {
927
+ background: rgba(255, 255, 255, 0.3);
928
+ transform: scale(1.1);
929
+ }
930
+
931
+ .toggle-panel-btn:active {
932
+ transform: scale(0.95);
933
+ }
934
+
935
+ .input-content {
936
+ padding: 20px;
937
+ transition: all 0.3s ease;
938
+ }
939
+
940
+ .input-content.collapsed {
941
+ display: none;
942
+ }
943
+
944
+ .input-group {
945
+ display: flex;
946
+ gap: 10px;
947
+ margin-bottom: 20px;
948
+ }
949
+
950
+ .command-input {
951
+ flex: 1;
952
+ padding: 12px 15px;
953
+ border: 2px solid #4ecdc4;
954
+ border-radius: 25px;
955
+ background: rgba(255, 255, 255, 0.1);
956
+ color: white;
957
+ font-family: 'Comic Neue', cursive;
958
+ font-size: 14px;
959
+ outline: none;
960
+ transition: all 0.3s ease;
961
+ }
962
+
963
+ .command-input::placeholder {
964
+ color: rgba(255, 255, 255, 0.7);
965
+ }
966
+
967
+ .command-input:focus {
968
+ border-color: #ff6b6b;
969
+ background: rgba(255, 255, 255, 0.2);
970
+ box-shadow: 0 0 15px rgba(255, 107, 107, 0.3);
971
+ }
972
+
973
+ .send-btn {
974
+ background: linear-gradient(45deg, #ff6b6b, #e55555);
975
+ border: none;
976
+ border-radius: 25px;
977
+ padding: 12px 20px;
978
+ color: white;
979
+ font-family: 'Comic Neue', cursive;
980
+ font-weight: bold;
981
+ font-size: 14px;
982
+ cursor: pointer;
983
+ transition: all 0.3s ease;
984
+ box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
985
+ }
986
+
987
+ .send-btn:hover {
988
+ background: linear-gradient(45deg, #e55555, #d63447);
989
+ transform: translateY(-2px);
990
+ box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4);
991
+ }
992
+
993
+ .send-btn:active {
994
+ transform: translateY(0);
995
+ }
996
+
997
+ .quick-commands {
998
+ margin-bottom: 20px;
999
+ }
1000
+
1001
+ .quick-commands h5 {
1002
+ margin: 0 0 10px 0;
1003
+ color: #4ecdc4;
1004
+ font-size: 0.9em;
1005
+ font-weight: bold;
1006
+ text-transform: uppercase;
1007
+ letter-spacing: 1px;
1008
+ }
1009
+
1010
+ .command-buttons {
1011
+ display: grid;
1012
+ grid-template-columns: 1fr 1fr;
1013
+ gap: 8px;
1014
+ }
1015
+
1016
+ .quick-cmd {
1017
+ background: rgba(78, 205, 196, 0.2);
1018
+ border: 1px solid #4ecdc4;
1019
+ border-radius: 20px;
1020
+ padding: 8px 12px;
1021
+ color: white;
1022
+ font-family: 'Comic Neue', cursive;
1023
+ font-size: 0.8em;
1024
+ cursor: pointer;
1025
+ transition: all 0.3s ease;
1026
+ text-align: center;
1027
+ }
1028
+
1029
+ .quick-cmd:hover {
1030
+ background: rgba(78, 205, 196, 0.4);
1031
+ border-color: #44a08d;
1032
+ transform: scale(1.05);
1033
+ }
1034
+
1035
+ .quick-cmd:active {
1036
+ transform: scale(0.95);
1037
+ }
1038
+
1039
+ .command-history {
1040
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
1041
+ padding-top: 15px;
1042
+ }
1043
+
1044
+ .command-history h5 {
1045
+ margin: 0 0 10px 0;
1046
+ color: #4ecdc4;
1047
+ font-size: 0.9em;
1048
+ font-weight: bold;
1049
+ text-transform: uppercase;
1050
+ letter-spacing: 1px;
1051
+ }
1052
+
1053
+ .history-list {
1054
+ max-height: 120px;
1055
+ overflow-y: auto;
1056
+ border-radius: 8px;
1057
+ background: rgba(0, 0, 0, 0.2);
1058
+ padding: 8px;
1059
+ }
1060
+
1061
+ .history-item {
1062
+ background: rgba(255, 255, 255, 0.1);
1063
+ border-radius: 15px;
1064
+ padding: 6px 12px;
1065
+ margin-bottom: 5px;
1066
+ font-size: 0.8em;
1067
+ color: rgba(255, 255, 255, 0.9);
1068
+ display: flex;
1069
+ justify-content: space-between;
1070
+ align-items: center;
1071
+ cursor: pointer;
1072
+ transition: all 0.3s ease;
1073
+ }
1074
+
1075
+ .history-item:hover {
1076
+ background: rgba(255, 255, 255, 0.2);
1077
+ transform: translateX(5px);
1078
+ }
1079
+
1080
+ .history-item:last-child {
1081
+ margin-bottom: 0;
1082
+ }
1083
+
1084
+ .history-command {
1085
+ flex: 1;
1086
+ white-space: nowrap;
1087
+ overflow: hidden;
1088
+ text-overflow: ellipsis;
1089
+ }
1090
+
1091
+ .history-time {
1092
+ color: rgba(255, 255, 255, 0.6);
1093
+ font-size: 0.7em;
1094
+ margin-left: 10px;
1095
+ }
1096
+
1097
+ /* 滚动条样式 */
1098
+ .history-list::-webkit-scrollbar {
1099
+ width: 4px;
1100
+ }
1101
+
1102
+ .history-list::-webkit-scrollbar-track {
1103
+ background: rgba(255, 255, 255, 0.1);
1104
+ border-radius: 2px;
1105
+ }
1106
+
1107
+ .history-list::-webkit-scrollbar-thumb {
1108
+ background: #4ecdc4;
1109
+ border-radius: 2px;
1110
+ }
1111
+
1112
+ .history-list::-webkit-scrollbar-thumb:hover {
1113
+ background: #44a08d;
1114
+ }
1115
+
1116
+ /* 面板折叠状态 */
1117
+ .keyboard-input-panel.collapsed {
1118
+ min-width: auto;
1119
+ width: auto;
1120
+ }
1121
+
1122
+ .keyboard-input-panel.collapsed .input-content {
1123
+ display: none;
1124
+ }
1125
+
1126
+ /* 面板动画 */
1127
+ @keyframes panelSlideIn {
1128
+ from {
1129
+ transform: translateX(100%);
1130
+ opacity: 0;
1131
+ }
1132
+ to {
1133
+ transform: translateX(0);
1134
+ opacity: 1;
1135
+ }
1136
+ }
1137
+
1138
+ .keyboard-input-panel {
1139
+ animation: panelSlideIn 0.5s ease-out;
1140
+ }
1141
+
1142
+ /* 响应式设计 */
1143
+ @media (max-width: 768px) {
1144
+ .keyboard-input-panel {
1145
+ bottom: 10px;
1146
+ right: 10px;
1147
+ left: 10px;
1148
+ min-width: auto;
1149
+ max-width: none;
1150
+ }
1151
+
1152
+ .command-buttons {
1153
+ grid-template-columns: 1fr;
1154
+ }
1155
+
1156
+ .input-group {
1157
+ flex-direction: column;
1158
+ }
1159
+
1160
+ .send-btn {
1161
+ align-self: flex-end;
1162
+ min-width: 100px;
1163
+ }
1164
+ }
voice.js ADDED
@@ -0,0 +1,667 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 语音识别类
2
+ class VoiceRecognition {
3
+ constructor() {
4
+ this.recognition = null;
5
+ this.isListening = false;
6
+ this.isSupported = false;
7
+ this.onResult = null;
8
+ this.onError = null;
9
+ this.onStatusChange = null;
10
+
11
+ // 错误重试控制
12
+ this.retryCount = 0;
13
+ this.maxRetries = 3;
14
+ this.retryDelay = 2000; // 2秒延迟
15
+ this.lastErrorTime = 0;
16
+ this.consecutiveErrors = 0;
17
+ this.isDisabled = false;
18
+
19
+ // 环境检测
20
+ this.isHuggingFaceSpace = this.detectHuggingFaceSpace();
21
+ this.isInIframe = window.parent !== window;
22
+ this.hasUserInteraction = false;
23
+
24
+ this.init();
25
+ }
26
+
27
+ init() {
28
+ // 环境兼容性检查
29
+ if (this.isHuggingFaceSpace || this.isInIframe) {
30
+ console.warn('检测到特殊环境,语音识别可能受限:', {
31
+ isHuggingFaceSpace: this.isHuggingFaceSpace,
32
+ isInIframe: this.isInIframe
33
+ });
34
+ this.showEnvironmentWarning();
35
+ }
36
+
37
+ // 检查浏览器支持
38
+ if ('webkitSpeechRecognition' in window) {
39
+ this.recognition = new webkitSpeechRecognition();
40
+ this.isSupported = true;
41
+ } else if ('SpeechRecognition' in window) {
42
+ this.recognition = new SpeechRecognition();
43
+ this.isSupported = true;
44
+ } else {
45
+ console.warn('浏览器不支持语音识别');
46
+ this.handleError('浏览器不支持语音识别');
47
+ return;
48
+ }
49
+
50
+ this.setupRecognition();
51
+
52
+ // 在特殊环境中,不自动启动语音识别
53
+ if (!this.isHuggingFaceSpace && !this.isInIframe) {
54
+ this.startListening();
55
+ } else {
56
+ console.log('在受限环境中,语音识别需要用户手动启动');
57
+ this.showNotification('在当前环境中,请点击页面任意位置启动语音识别', 'info');
58
+ }
59
+ }
60
+
61
+ detectHuggingFaceSpace() {
62
+ // 检测是否在 Hugging Face Space 环境中
63
+ const hostname = window.location.hostname;
64
+ const userAgent = navigator.userAgent.toLowerCase();
65
+
66
+ return hostname.includes('hf.space') ||
67
+ hostname.includes('huggingface.co') ||
68
+ hostname.includes('gradio.live') ||
69
+ userAgent.includes('huggingface');
70
+ }
71
+
72
+ showEnvironmentWarning() {
73
+ // 显示环境警告
74
+ console.warn('当前运行在受限环境中,语音识别功能可能不稳定');
75
+
76
+ // 调整配置以适应受限环境
77
+ this.maxRetries = 1; // 减少重试次数
78
+ this.retryDelay = 5000; // 增加延迟时间
79
+
80
+ setTimeout(() => {
81
+ this.showNotification(
82
+ '检测到受限环境,语音功能可能需要手动激活。如遇问题请使用键盘输入(按F9)',
83
+ 'info'
84
+ );
85
+ }, 2000);
86
+ }
87
+
88
+ setupRecognition() {
89
+ // 配置语音识别
90
+ this.recognition.continuous = true;
91
+ this.recognition.interimResults = false;
92
+ this.recognition.lang = 'en-US'; // 使用英文识别食物名称
93
+ this.recognition.maxAlternatives = 3;
94
+
95
+ // 事件监听
96
+ this.recognition.onstart = () => {
97
+ this.isListening = true;
98
+ this.updateStatus('listening');
99
+ console.log('语音识别开始');
100
+ };
101
+
102
+ this.recognition.onresult = (event) => {
103
+ this.handleResult(event);
104
+ };
105
+
106
+ this.recognition.onerror = (event) => {
107
+ this.handleError(event.error);
108
+ };
109
+
110
+ this.recognition.onend = () => {
111
+ this.isListening = false;
112
+ this.updateStatus('ready');
113
+ console.log('语音识别结束');
114
+
115
+ // 智能重启逻辑 - 避免在错误频发时无限重试
116
+ if (this.shouldAttemptRestart()) {
117
+ const delay = this.calculateRestartDelay();
118
+ console.log(`准备在 ${delay}ms 后重新启动语音识别...`);
119
+
120
+ setTimeout(() => {
121
+ if (this.isSupported && !this.isListening && !this.isDisabled) {
122
+ this.startListening();
123
+ }
124
+ }, delay);
125
+ } else {
126
+ console.log('由于频繁错误,暂停自动重启语音识别');
127
+ this.showNotification('语音识别暂停,请点击页面重新激活或使用键盘输入(F9)', 'warning');
128
+ }
129
+ };
130
+ }
131
+
132
+ handleResult(event) {
133
+ this.updateStatus('processing');
134
+
135
+ let finalTranscript = '';
136
+ let interimTranscript = '';
137
+ let confidence = 0;
138
+
139
+ // 处理识别结果
140
+ for (let i = event.resultIndex; i < event.results.length; i++) {
141
+ const transcript = event.results[i][0].transcript;
142
+ confidence = event.results[i][0].confidence || 0;
143
+
144
+ if (event.results[i].isFinal) {
145
+ finalTranscript += transcript;
146
+ } else {
147
+ interimTranscript += transcript;
148
+ }
149
+ }
150
+
151
+ // 显示实时语音输入
152
+ const currentText = finalTranscript || interimTranscript;
153
+ if (currentText) {
154
+ this.showVoiceInput(currentText, confidence, !finalTranscript);
155
+ }
156
+
157
+ // 如果有最终结果,触发回调
158
+ if (finalTranscript && this.onResult) {
159
+ console.log('语音识别结果:', finalTranscript);
160
+ this.onResult(finalTranscript.trim());
161
+
162
+ // 短暂显示识别成功状态
163
+ setTimeout(() => {
164
+ this.hideVoiceInput();
165
+ }, 2000);
166
+ }
167
+
168
+ this.updateStatus('ready');
169
+ }
170
+
171
+ handleError(error) {
172
+ console.error('语音识别错误:', error);
173
+ this.updateStatus('error');
174
+
175
+ // 记录错误时间和连续错误次数
176
+ const now = Date.now();
177
+ if (now - this.lastErrorTime < 5000) { // 5秒内的错误视为连续错误
178
+ this.consecutiveErrors++;
179
+ } else {
180
+ this.consecutiveErrors = 1;
181
+ }
182
+ this.lastErrorTime = now;
183
+
184
+ if (this.onError) {
185
+ this.onError(error);
186
+ }
187
+
188
+ // 特殊处理频繁的 aborted 错误
189
+ if (error === 'aborted' && this.consecutiveErrors >= 3) {
190
+ console.warn('检测到频繁的 aborted 错误,可能是环境限制导致');
191
+ this.handleAbortedErrorLoop();
192
+ return;
193
+ }
194
+
195
+ // 根据错误类型处理
196
+ switch (error) {
197
+ case 'not-allowed':
198
+ this.showPermissionError();
199
+ this.isDisabled = true; // 禁用自动重启
200
+ break;
201
+ case 'no-speech':
202
+ // 没有检测到语音,继续监听(但要控制频率)
203
+ if (this.consecutiveErrors < 5) {
204
+ setTimeout(() => {
205
+ if (this.isSupported && !this.isDisabled) {
206
+ this.startListening();
207
+ }
208
+ }, 1000);
209
+ }
210
+ break;
211
+ case 'network':
212
+ this.showNetworkError();
213
+ if (this.consecutiveErrors < 3) {
214
+ setTimeout(() => {
215
+ if (this.isSupported && !this.isDisabled) {
216
+ this.startListening();
217
+ }
218
+ }, 3000);
219
+ }
220
+ break;
221
+ case 'aborted':
222
+ this.handleAbortedError();
223
+ break;
224
+ default:
225
+ // 其他错误,谨慎重试
226
+ if (this.consecutiveErrors < 3) {
227
+ setTimeout(() => {
228
+ if (this.isSupported && !this.isDisabled) {
229
+ this.startListening();
230
+ }
231
+ }, this.retryDelay);
232
+ }
233
+ break;
234
+ }
235
+ }
236
+
237
+ handleAbortedError() {
238
+ console.log('处理 aborted 错误,连续错误次数:', this.consecutiveErrors);
239
+
240
+ if (this.consecutiveErrors >= 3) {
241
+ this.handleAbortedErrorLoop();
242
+ } else {
243
+ // 延长重试时间
244
+ const delay = this.retryDelay * this.consecutiveErrors;
245
+ console.log(`Aborted 错误,${delay}ms 后重试`);
246
+
247
+ setTimeout(() => {
248
+ if (this.isSupported && !this.isDisabled) {
249
+ this.startListening();
250
+ }
251
+ }, delay);
252
+ }
253
+ }
254
+
255
+ handleAbortedErrorLoop() {
256
+ console.warn('检测到 aborted 错误循环,暂停自动重启');
257
+ this.isDisabled = true;
258
+
259
+ this.showNotification(
260
+ '语音识别在当前环境中不稳定,已暂停自动重启。\n请使用键盘输入(按F9键)或点击页面手动重启',
261
+ 'warning'
262
+ );
263
+
264
+ // 5分钟后重新尝试
265
+ setTimeout(() => {
266
+ this.resetErrorState();
267
+ this.showNotification('语音识别已重新启用,点击页面激活', 'info');
268
+ }, 300000); // 5分钟
269
+ }
270
+
271
+ shouldAttemptRestart() {
272
+ // 判断是否应该尝试重启
273
+ if (this.isDisabled) return false;
274
+ if (this.consecutiveErrors >= 5) return false;
275
+ if (this.isHuggingFaceSpace && this.consecutiveErrors >= 2) return false;
276
+
277
+ return true;
278
+ }
279
+
280
+ calculateRestartDelay() {
281
+ // 计算重启延迟时间
282
+ let baseDelay = this.retryDelay;
283
+
284
+ // 根据连续错误次数增加延迟
285
+ const multiplier = Math.min(this.consecutiveErrors, 5);
286
+ baseDelay *= multiplier;
287
+
288
+ // 在受限环境中增加额外延迟
289
+ if (this.isHuggingFaceSpace || this.isInIframe) {
290
+ baseDelay *= 2;
291
+ }
292
+
293
+ return Math.min(baseDelay, 30000); // 最大30秒
294
+ }
295
+
296
+ resetErrorState() {
297
+ // 重置错误状态
298
+ this.consecutiveErrors = 0;
299
+ this.retryCount = 0;
300
+ this.isDisabled = false;
301
+ this.lastErrorTime = 0;
302
+ console.log('错误状态已重置');
303
+ }
304
+
305
+ startListening() {
306
+ if (!this.isSupported) {
307
+ this.showUnsupportedError();
308
+ return;
309
+ }
310
+
311
+ if (this.isDisabled) {
312
+ console.log('语音识别已禁用,需要手动重启');
313
+ return;
314
+ }
315
+
316
+ // 增强的状态检查
317
+ if (this.isListening) {
318
+ console.log('语音识别已在运行中,跳过启动');
319
+ return;
320
+ }
321
+
322
+ // 在受限环境中需要用户交互才能启动
323
+ if ((this.isHuggingFaceSpace || this.isInIframe) && !this.hasUserInteraction) {
324
+ this.waitForUserInteraction();
325
+ return;
326
+ }
327
+
328
+ try {
329
+ // 确保之前的识别已完全停止
330
+ if (this.recognition) {
331
+ this.recognition.abort(); // 强制停止之前的识别
332
+ }
333
+
334
+ // 根据环境调整延迟
335
+ const delay = this.isHuggingFaceSpace ? 500 : 100;
336
+
337
+ setTimeout(() => {
338
+ try {
339
+ if (!this.isListening && !this.isDisabled) {
340
+ this.recognition.start();
341
+ console.log('语音识别启动尝试');
342
+ }
343
+ } catch (error) {
344
+ if (error.message.includes('recognition has already started')) {
345
+ console.log('识别已启动,尝试停止后重新启动');
346
+ this.recognition.stop();
347
+ setTimeout(() => {
348
+ try {
349
+ if (!this.isDisabled) {
350
+ this.recognition.start();
351
+ }
352
+ } catch (retryError) {
353
+ console.error('重试启动失败:', retryError);
354
+ this.handleError('start-failed');
355
+ }
356
+ }, 1000);
357
+ } else {
358
+ throw error;
359
+ }
360
+ }
361
+ }, delay);
362
+
363
+ } catch (error) {
364
+ console.error('启动语音识别失败:', error);
365
+ this.handleError('start-failed');
366
+ }
367
+ }
368
+
369
+ waitForUserInteraction() {
370
+ if (this.hasUserInteraction) return;
371
+
372
+ console.log('等待用户交互以启动语音识别');
373
+
374
+ const startOnInteraction = () => {
375
+ this.hasUserInteraction = true;
376
+ console.log('检测到用户交互,启动语音识别');
377
+ this.startListening();
378
+
379
+ // 移除事件监听器
380
+ document.removeEventListener('click', startOnInteraction);
381
+ document.removeEventListener('keydown', startOnInteraction);
382
+ document.removeEventListener('touchstart', startOnInteraction);
383
+ };
384
+
385
+ // 监听用户交互事件
386
+ document.addEventListener('click', startOnInteraction, { once: true });
387
+ document.addEventListener('keydown', startOnInteraction, { once: true });
388
+ document.addEventListener('touchstart', startOnInteraction, { once: true });
389
+
390
+ this.showNotification('请点击页面任意位置激活语音识别', 'info');
391
+ }
392
+
393
+ stopListening() {
394
+ if (this.recognition && this.isListening) {
395
+ this.recognition.stop();
396
+ }
397
+ }
398
+
399
+ // 重启语音识别
400
+ restart() {
401
+ console.log('手动重启语音识别系统...');
402
+
403
+ // 重置错误状态
404
+ this.resetErrorState();
405
+
406
+ // 强制停止当前识别
407
+ if (this.recognition) {
408
+ this.isListening = false;
409
+ try {
410
+ this.recognition.abort();
411
+ } catch (error) {
412
+ console.log('停止识别时出错:', error);
413
+ }
414
+ }
415
+
416
+ // 清除可能存在的定时器
417
+ clearTimeout(this.restartTimer);
418
+
419
+ // 等待一段时间后重新启动
420
+ this.restartTimer = setTimeout(() => {
421
+ if (this.isSupported) {
422
+ console.log('执行重启...');
423
+ this.startListening();
424
+ this.showNotification('语音识别已重启', 'info');
425
+ }
426
+ }, 1000);
427
+ }
428
+
429
+ // 手动激活语音识别(用于用户点击按钮)
430
+ manualActivate() {
431
+ console.log('手动激活语音识别');
432
+ this.hasUserInteraction = true;
433
+ this.resetErrorState();
434
+
435
+ if (this.isSupported) {
436
+ this.startListening();
437
+ this.showNotification('语音识别已激活', 'info');
438
+ } else {
439
+ this.showNotification('当前浏览器不支持语音识别', 'error');
440
+ }
441
+ }
442
+
443
+ // 永久禁用语音识别
444
+ disable() {
445
+ console.log('禁用语音识别');
446
+ this.isDisabled = true;
447
+
448
+ if (this.recognition && this.isListening) {
449
+ this.recognition.abort();
450
+ }
451
+
452
+ this.showNotification('语音识别已禁用', 'warning');
453
+ }
454
+
455
+ // 启用语音识别
456
+ enable() {
457
+ console.log('启用语音识别');
458
+ this.resetErrorState();
459
+
460
+ if (this.isSupported) {
461
+ this.showNotification('语音识别已启用,点击页面激活', 'info');
462
+
463
+ // 在受限环境中等待用户交互
464
+ if (this.isHuggingFaceSpace || this.isInIframe) {
465
+ this.hasUserInteraction = false;
466
+ this.waitForUserInteraction();
467
+ } else {
468
+ this.startListening();
469
+ }
470
+ }
471
+ }
472
+
473
+ updateStatus(status) {
474
+ if (this.onStatusChange) {
475
+ this.onStatusChange(status);
476
+ }
477
+ }
478
+
479
+ showVoiceInput(text, confidence = 0, isInterim = false) {
480
+ const voiceDisplay = document.getElementById('voiceInputDisplay');
481
+ const speechText = document.getElementById('speechText');
482
+ const speechConfidence = document.getElementById('speechConfidence');
483
+ const speechBubble = voiceDisplay.querySelector('.speech-bubble');
484
+
485
+ if (!voiceDisplay || !speechText) return;
486
+
487
+ // 显示语音输入框
488
+ voiceDisplay.style.display = 'block';
489
+
490
+ // 更新文本内容
491
+ speechText.textContent = text;
492
+
493
+ // 显示置信度
494
+ if (confidence > 0) {
495
+ speechConfidence.textContent = `置信度: ${Math.round(confidence * 100)}%`;
496
+ } else {
497
+ speechConfidence.textContent = isInterim ? '正在识别...' : '';
498
+ }
499
+
500
+ // 更新样式状态
501
+ speechBubble.className = 'speech-bubble';
502
+ if (isInterim) {
503
+ speechBubble.classList.add('listening');
504
+ } else {
505
+ speechBubble.classList.add('recognized');
506
+ }
507
+
508
+ // 清除之前的隐藏定时器
509
+ if (this.hideTimer) {
510
+ clearTimeout(this.hideTimer);
511
+ }
512
+
513
+ // 如果是临时结果,设置自动隐藏
514
+ if (isInterim) {
515
+ this.hideTimer = setTimeout(() => {
516
+ this.hideVoiceInput();
517
+ }, 3000);
518
+ }
519
+ }
520
+
521
+ hideVoiceInput() {
522
+ const voiceDisplay = document.getElementById('voiceInputDisplay');
523
+ if (voiceDisplay) {
524
+ voiceDisplay.style.display = 'none';
525
+ }
526
+
527
+ if (this.hideTimer) {
528
+ clearTimeout(this.hideTimer);
529
+ this.hideTimer = null;
530
+ }
531
+ }
532
+
533
+ showPermissionError() {
534
+ this.showNotification('需要麦克风权限才能使用语音功能', 'error');
535
+ }
536
+
537
+ showNetworkError() {
538
+ this.showNotification('网络连接错误,语音识别暂时不可用', 'error');
539
+ }
540
+
541
+ showUnsupportedError() {
542
+ this.showNotification('您的浏览器不支持语音识别功能', 'error');
543
+ }
544
+
545
+ showNotification(message, type = 'info') {
546
+ // 创建通知元素
547
+ const notification = document.createElement('div');
548
+ notification.className = `voice-notification ${type}`;
549
+ notification.textContent = message;
550
+
551
+ // 样式
552
+ Object.assign(notification.style, {
553
+ position: 'fixed',
554
+ top: '20px',
555
+ right: '20px',
556
+ background: type === 'error' ? '#e74c3c' : '#4ecdc4',
557
+ color: 'white',
558
+ padding: '15px 20px',
559
+ borderRadius: '10px',
560
+ boxShadow: '0 4px 15px rgba(0,0,0,0.2)',
561
+ zIndex: '10000',
562
+ fontSize: '14px',
563
+ maxWidth: '300px',
564
+ animation: 'slideInRight 0.3s ease'
565
+ });
566
+
567
+ document.body.appendChild(notification);
568
+
569
+ // 自动移除
570
+ setTimeout(() => {
571
+ notification.style.animation = 'slideOutRight 0.3s ease';
572
+ setTimeout(() => {
573
+ if (notification.parentNode) {
574
+ notification.parentNode.removeChild(notification);
575
+ }
576
+ }, 300);
577
+ }, 5000);
578
+ }
579
+
580
+ // 测试语音识别功能
581
+ test() {
582
+ if (!this.isSupported) {
583
+ console.log('语音识别不支持');
584
+ return false;
585
+ }
586
+
587
+ console.log('语音识别测试开始...');
588
+
589
+ // 设置测试回调
590
+ const originalOnResult = this.onResult;
591
+ this.onResult = (transcript) => {
592
+ console.log('测试结果:', transcript);
593
+ this.onResult = originalOnResult;
594
+ };
595
+
596
+ return true;
597
+ }
598
+
599
+ // 获取支持的语言列表
600
+ getSupportedLanguages() {
601
+ return [
602
+ { code: 'en-US', name: 'English (US)' },
603
+ { code: 'zh-CN', name: '中文 (普通话)' },
604
+ { code: 'ja-JP', name: '日本語' },
605
+ { code: 'ko-KR', name: '한국어' },
606
+ { code: 'fr-FR', name: 'Français' },
607
+ { code: 'de-DE', name: 'Deutsch' },
608
+ { code: 'es-ES', name: 'Español' },
609
+ { code: 'it-IT', name: 'Italiano' }
610
+ ];
611
+ }
612
+
613
+ // 切换语言
614
+ setLanguage(langCode) {
615
+ if (this.recognition) {
616
+ this.recognition.lang = langCode;
617
+ console.log(`语音识别语言切换为: ${langCode}`);
618
+ }
619
+ }
620
+
621
+ // 获取当前状态
622
+ getStatus() {
623
+ return {
624
+ isSupported: this.isSupported,
625
+ isListening: this.isListening,
626
+ isDisabled: this.isDisabled,
627
+ hasUserInteraction: this.hasUserInteraction,
628
+ consecutiveErrors: this.consecutiveErrors,
629
+ isHuggingFaceSpace: this.isHuggingFaceSpace,
630
+ isInIframe: this.isInIframe,
631
+ currentLanguage: this.recognition ? this.recognition.lang : null
632
+ };
633
+ }
634
+ }
635
+
636
+ // 添加CSS动画
637
+ const style = document.createElement('style');
638
+ style.textContent = `
639
+ @keyframes slideInRight {
640
+ from {
641
+ transform: translateX(100%);
642
+ opacity: 0;
643
+ }
644
+ to {
645
+ transform: translateX(0);
646
+ opacity: 1;
647
+ }
648
+ }
649
+
650
+ @keyframes slideOutRight {
651
+ from {
652
+ transform: translateX(0);
653
+ opacity: 1;
654
+ }
655
+ to {
656
+ transform: translateX(100%);
657
+ opacity: 0;
658
+ }
659
+ }
660
+
661
+ .voice-notification {
662
+ font-family: 'Comic Neue', cursive;
663
+ font-weight: bold;
664
+ word-wrap: break-word;
665
+ }
666
+ `;
667
+ document.head.appendChild(style);