| |
| class MagicPotGame { |
| constructor() { |
| this.canvas = document.getElementById('gameCanvas'); |
| this.ctx = this.canvas.getContext('2d'); |
| this.gameState = 'idle'; |
| this.currentFood = null; |
| this.foodCount = 0; |
| |
| this.lastTime = 0; |
| |
| |
| this.config = { |
| maxFoodCount: 100, |
| maxParticles: 150, |
| maxAnimals: 8, |
| animalSpawnThreshold: 5, |
| easterEggThreshold: 30, |
| particleSpawnRate: 1, |
| cookingDuration: 3000, |
| cleanupInterval: 5000, |
| maxGroundedParticles: 80 |
| }; |
| |
| |
| this.voice = new VoiceRecognition(); |
| this.keyboard = new KeyboardInputHandler(); |
| this.pot = new MagicPot(this.canvas.width / 2, this.canvas.height / 2 + 50); |
| this.particleSystem = new ParticleSystem(); |
| this.animalSystem = new AnimalSystem(this.canvas); |
| this.textToImage = new TextToImageAPI(); |
| this.audio = window.audioManager; |
| |
| |
| this.lastFoodSpawnTime = 0; |
| this.foodSpawnInterval = 500; |
| |
| |
| this.lastCleanupTime = 0; |
| this.performanceStats = { |
| particles: 0, |
| animals: 0, |
| fps: 0, |
| lastFpsTime: 0, |
| frameCount: 0 |
| }; |
| |
| this.init(); |
| } |
| |
| init() { |
| this.setupEventListeners(); |
| this.setupCanvas(); |
| this.gameLoop(); |
| this.updateUI(); |
| |
| |
| this.showDebug('游戏初始化完成,等待语音命令...'); |
| |
| |
| this.startPerformanceMonitoring(); |
| } |
| |
| setupEventListeners() { |
| |
| this.voice.onResult = (transcript) => { |
| this.handleVoiceCommand(transcript); |
| }; |
| |
| this.voice.onError = (error) => { |
| this.showDebug(`语音识别错误: ${error}`); |
| }; |
| |
| this.voice.onStatusChange = (status) => { |
| this.updateVoiceStatus(status); |
| }; |
| |
| |
| this.keyboard.onCommand = (command) => { |
| this.handleKeyboardCommand(command); |
| }; |
| |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'F12') { |
| this.toggleDebug(); |
| } else if (e.key === 'F11') { |
| this.testAudio(); |
| } else if (e.key === 'F10') { |
| this.toggleAudio(); |
| } else if (e.key === 'F9') { |
| e.preventDefault(); |
| this.toggleKeyboardActivation(); |
| } else if (e.key === 'F8') { |
| this.toggleKeyboardPanel(); |
| } else if (e.key === 'F7') { |
| this.clearCommandHistory(); |
| } else if (e.key === 'F6') { |
| this.restartVoice(); |
| } else if (e.key === 'F3') { |
| this.manualActivateVoice(); |
| } else if (e.key === 'F5') { |
| this.resetKeyboardPanelPosition(); |
| } else if (e.key === 'F4') { |
| this.adjustFoodSpawnSpeed(); |
| } else if (e.key === 'a' && e.altKey) { |
| e.preventDefault(); |
| this.forceSpawnAnimal(); |
| } |
| }); |
| |
| |
| const audioButton = document.getElementById('audioButton'); |
| if (audioButton) { |
| audioButton.addEventListener('click', () => { |
| this.toggleAudio(); |
| }); |
| this.updateAudioButton(); |
| } |
| |
| |
| const voiceActivateButton = document.getElementById('voiceActivateButton'); |
| if (voiceActivateButton) { |
| voiceActivateButton.addEventListener('click', () => { |
| this.manualActivateVoice(); |
| }); |
| |
| this.updateVoiceActivateButton(); |
| } |
| |
| |
| const helpButton = document.getElementById('helpButton'); |
| if (helpButton) { |
| helpButton.addEventListener('click', () => { |
| this.showVoiceHelp(); |
| }); |
| } |
| |
| |
| window.addEventListener('resize', () => { |
| this.resizeCanvas(); |
| }); |
| } |
| |
| setupCanvas() { |
| |
| const dpr = window.devicePixelRatio || 1; |
| const rect = this.canvas.getBoundingClientRect(); |
| |
| this.canvas.width = rect.width * dpr; |
| this.canvas.height = rect.height * dpr; |
| this.ctx.scale(dpr, dpr); |
| |
| this.canvas.style.width = rect.width + 'px'; |
| this.canvas.style.height = rect.height + 'px'; |
| } |
| |
| handleVoiceCommand(transcript) { |
| const command = transcript.toLowerCase().trim(); |
| this.showDebug(`语音命令: "${command}"`); |
| this.processCommand(command, '语音'); |
| } |
| |
| handleKeyboardCommand(command) { |
| const normalizedCommand = command.toLowerCase().trim(); |
| this.showDebug(`键盘命令: "${command}"`); |
| this.processCommand(normalizedCommand, '键盘'); |
| } |
| |
| processCommand(command, source) { |
| |
| |
| |
| if (this.matchActivationCommand(command)) { |
| this.activatePot(); |
| this.showDebug(`通过${source}激活魔法锅`); |
| return; |
| } |
| |
| |
| if (this.matchStopCommand(command)) { |
| this.stopCooking(); |
| this.showDebug(`通过${source}停止烹饪`); |
| return; |
| } |
| |
| |
| if (this.gameState === 'activated') { |
| const foodName = this.extractFoodName(command); |
| if (foodName) { |
| this.startCooking(foodName); |
| this.showDebug(`通过${source}开始烹饪: ${foodName}`); |
| } else { |
| this.showDebug(`未识别的食物: "${command}" (来源: ${source})`); |
| } |
| } else { |
| |
| if (this.isValidFoodCommand(command)) { |
| this.showDebug(`请先激活魔法锅再烹饪食物 (来源: ${source})`); |
| this.showSpecialMessage('请先说 "cook cook cook pot" 激活魔法锅'); |
| } |
| } |
| } |
| |
| isValidFoodCommand(command) { |
| |
| const foods = [ |
| 'apple', 'banana', 'orange', 'strawberry', 'watermelon', 'grape', |
| 'pizza', 'burger', 'bread', 'rice', 'noodles', 'cake', 'cookie', |
| 'cheese', 'fish', 'chicken', 'carrot', 'tomato', 'corn', 'broccoli', |
| 'potato', 'onion' |
| ]; |
| |
| return foods.some(food => command.includes(food)); |
| } |
| |
| matchActivationCommand(command) { |
| |
| const patterns = [ |
| /cook.*cook.*cook.*pot/, |
| /cook.*pot.*cook.*cook/, |
| /pot.*cook.*cook.*cook/, |
| /cook\s+cook\s+cook\s+pot/, |
| /cook.*three.*times.*pot/, |
| /magic.*pot.*cook/ |
| ]; |
| |
| return patterns.some(pattern => pattern.test(command)); |
| } |
| |
| matchStopCommand(command) { |
| |
| const patterns = [ |
| /stop.*stop.*stop.*pot/, |
| /stop.*pot.*stop.*stop/, |
| /pot.*stop.*stop.*stop/, |
| /stop\s+stop\s+stop\s+pot/, |
| /stop.*three.*times.*pot/, |
| /magic.*pot.*stop/ |
| ]; |
| |
| return patterns.some(pattern => pattern.test(command)); |
| } |
| |
| extractFoodName(command) { |
| |
| const foods = [ |
| 'apple', 'banana', 'orange', 'strawberry', 'watermelon', 'grape', |
| 'pizza', 'burger', 'bread', 'rice', 'noodles', 'cake', 'cookie', |
| 'cheese', 'fish', 'chicken', 'carrot', 'tomato', 'corn', 'broccoli', |
| 'potato', 'onion' |
| ]; |
| |
| |
| for (const food of foods) { |
| if (command.includes(food)) { |
| return food; |
| } |
| } |
| |
| |
| for (const food of foods) { |
| if (this.fuzzyMatch(command, food)) { |
| return food; |
| } |
| } |
| |
| return null; |
| } |
| |
| fuzzyMatch(text, target) { |
| |
| const threshold = 0.7; |
| const similarity = this.calculateSimilarity(text, target); |
| return similarity >= threshold; |
| } |
| |
| calculateSimilarity(str1, str2) { |
| |
| const longer = str1.length > str2.length ? str1 : str2; |
| const shorter = str1.length > str2.length ? str2 : str1; |
| |
| if (longer.length === 0) return 1.0; |
| |
| const distance = this.levenshteinDistance(longer, shorter); |
| return (longer.length - distance) / longer.length; |
| } |
| |
| levenshteinDistance(str1, str2) { |
| |
| const matrix = []; |
| |
| for (let i = 0; i <= str2.length; i++) { |
| matrix[i] = [i]; |
| } |
| |
| for (let j = 0; j <= str1.length; j++) { |
| matrix[0][j] = j; |
| } |
| |
| for (let i = 1; i <= str2.length; i++) { |
| for (let j = 1; j <= str1.length; j++) { |
| if (str2.charAt(i - 1) === str1.charAt(j - 1)) { |
| matrix[i][j] = matrix[i - 1][j - 1]; |
| } else { |
| matrix[i][j] = Math.min( |
| matrix[i - 1][j - 1] + 1, |
| matrix[i][j - 1] + 1, |
| matrix[i - 1][j] + 1 |
| ); |
| } |
| } |
| } |
| |
| return matrix[str2.length][str1.length]; |
| } |
| |
| activatePot() { |
| if (this.gameState === 'idle') { |
| this.gameState = 'activated'; |
| this.pot.activate(); |
| this.updateUI(); |
| this.showDebug('魔法锅已激活!'); |
| this.showSpecialMessage('🪄 魔法锅已激活!\n现在说出食物名称开始烹饪'); |
| |
| |
| if (this.audio) { |
| this.audio.playPotActivate(); |
| } |
| } |
| } |
| |
| async startCooking(foodName) { |
| if (this.gameState !== 'activated') return; |
| |
| this.gameState = 'cooking'; |
| this.currentFood = foodName; |
| this.pot.startCooking(foodName); |
| this.updateUI(); |
| |
| this.showDebug(`开始烹饪: ${foodName}`); |
| |
| |
| if (this.audio) { |
| this.audio.playCooking(); |
| |
| this.cookingSoundInterval = setInterval(() => { |
| if (this.gameState === 'cooking') { |
| this.audio.playCooking(); |
| } |
| }, 1000); |
| } |
| |
| try { |
| |
| await this.generateAssets(foodName); |
| |
| |
| setTimeout(() => { |
| this.startOverflowing(); |
| }, this.config.cookingDuration); |
| |
| } catch (error) { |
| this.showDebug(`生成素材失败: ${error.message}`); |
| |
| setTimeout(() => { |
| this.startOverflowing(); |
| }, this.config.cookingDuration); |
| } |
| } |
| |
| async generateAssets(foodName) { |
| |
| this.updateCookingProgress(0); |
| |
| |
| this.updateCookingProgress(30); |
| const foodImage = await this.textToImage.generateFood(foodName); |
| |
| |
| this.updateCookingProgress(60); |
| const animalImage = await this.textToImage.generateAnimal(foodName); |
| |
| |
| this.pot.setFoodAssets(foodImage, animalImage); |
| this.updateCookingProgress(100); |
| |
| this.showDebug(`素材生成完成: ${foodName}`); |
| } |
| |
| startOverflowing() { |
| this.gameState = 'overflowing'; |
| this.pot.startOverflowing(); |
| this.foodCount = 0; |
| this.updateUI(); |
| this.showDebug('食物开始涌出!'); |
| |
| console.log(`开始溢出,食物计数重置为: ${this.foodCount}`); |
| |
| |
| if (this.cookingSoundInterval) { |
| clearInterval(this.cookingSoundInterval); |
| this.cookingSoundInterval = null; |
| } |
| } |
| |
| stopCooking() { |
| if (this.gameState === 'overflowing') { |
| this.gameState = 'stopped'; |
| this.pot.stopOverflowing(); |
| this.updateUI(); |
| this.showDebug('停止烹饪'); |
| |
| |
| if (this.audio) { |
| this.audio.playStopCooking(); |
| } |
| |
| |
| if (this.cookingSoundInterval) { |
| clearInterval(this.cookingSoundInterval); |
| this.cookingSoundInterval = null; |
| } |
| |
| |
| this.showGameStats(); |
| |
| |
| setTimeout(() => { |
| this.resetGame(); |
| }, 5000); |
| } |
| } |
| |
| resetGame() { |
| this.gameState = 'idle'; |
| this.currentFood = null; |
| this.foodCount = 0; |
| this.pot.reset(); |
| this.particleSystem.clear(); |
| this.animalSystem.clear(); |
| this.updateUI(); |
| this.showDebug('游戏已重置,可以重新开始'); |
| } |
| |
| showGameStats() { |
| const stats = { |
| food: this.currentFood, |
| count: this.foodCount, |
| animals: this.animalSystem.getAnimalCount(), |
| particles: this.particleSystem.getParticleCount() |
| }; |
| |
| const message = `本轮统计:\n食物:${stats.food}\n数量:${stats.count}\n动物:${stats.animals}只`; |
| this.showSpecialMessage(message); |
| this.showDebug(`游戏统计: ${JSON.stringify(stats)}`); |
| } |
| |
| gameLoop(currentTime = 0) { |
| const deltaTime = currentTime - this.lastTime; |
| this.lastTime = currentTime; |
| |
| this.update(deltaTime); |
| this.render(); |
| |
| |
| this.updatePerformanceStats(currentTime); |
| this.performCleanupIfNeeded(currentTime); |
| |
| requestAnimationFrame((time) => this.gameLoop(time)); |
| } |
| |
| update(deltaTime) { |
| |
| this.pot.update(deltaTime); |
| |
| |
| this.particleSystem.update(deltaTime); |
| |
| |
| this.animalSystem.update(deltaTime); |
| |
| |
| if (this.gameState === 'overflowing') { |
| const currentTime = Date.now(); |
| if (currentTime - this.lastFoodSpawnTime >= this.foodSpawnInterval) { |
| this.spawnFoodParticles(); |
| this.lastFoodSpawnTime = currentTime; |
| |
| |
| this.checkAnimalSpawn(); |
| this.checkEasterEggs(); |
| } |
| } |
| } |
| |
| spawnFoodParticles() { |
| |
| if (this.particleSystem.getParticleCount() >= this.config.maxParticles) { |
| this.showDebug('已达到最大粒子数量,停止生成新粒子'); |
| return; |
| } |
| |
| |
| if (this.foodCount >= this.config.maxFoodCount) { |
| this.showDebug('已达到最大食物数量,自动停止烹饪'); |
| this.stopCooking(); |
| return; |
| } |
| |
| |
| const spawnCount = Math.min( |
| this.config.particleSpawnRate, |
| this.config.maxParticles - this.particleSystem.getParticleCount(), |
| this.config.maxFoodCount - this.foodCount |
| ); |
| |
| for (let i = 0; i < spawnCount; i++) { |
| const particle = this.particleSystem.createFoodParticle( |
| this.pot.x, |
| this.pot.y - 50, |
| this.currentFood |
| ); |
| |
| |
| this.foodCount++; |
| |
| console.log(`食物粒子已创建: ${this.currentFood}, 当前计数: ${this.foodCount}`); |
| |
| |
| if (this.audio && Math.random() < 0.3) { |
| this.audio.playRandomFoodPop(); |
| } |
| } |
| |
| |
| this.updateUI(); |
| } |
| |
| checkAnimalSpawn() { |
| |
| if (this.foodCount >= this.config.animalSpawnThreshold) { |
| |
| if (this.foodCount % 2 === 0 && Math.random() < 0.3) { |
| this.showDebug(`食物数量达到${this.foodCount},尝试生成动物`); |
| this.spawnAnimal(); |
| } |
| } else { |
| |
| if (this.foodCount % 2 === 0) { |
| const remaining = this.config.animalSpawnThreshold - this.foodCount; |
| this.showDebug(`还需要${remaining}个食物才能出现动物(当前:${this.foodCount})`); |
| } |
| } |
| } |
| |
| checkEasterEggs() { |
| |
| if ((this.currentFood === 'strawberry' || this.currentFood === 'watermelon') && |
| this.foodCount >= this.config.easterEggThreshold) { |
| |
| |
| const hasGirl = this.animalSystem.animals.some(animal => animal.type === 'girl'); |
| |
| if (!hasGirl && this.foodCount % 15 === 0) { |
| this.spawnLittleGirl(); |
| this.showSpecialMessage('小女孩被美味的水果吸引过来了!'); |
| } |
| } |
| |
| |
| this.checkSecretEasterEggs(); |
| } |
| |
| checkSecretEasterEggs() { |
| |
| if (this.foodCount === 100) { |
| this.createCelebrationEffect(); |
| this.showSpecialMessage('恭喜!你制作了100个食物!'); |
| } |
| |
| |
| if (this.currentFood === 'cake' && this.foodCount >= 20) { |
| this.createBirthdayEffect(); |
| } |
| } |
| |
| spawnAnimal() { |
| |
| if (this.animalSystem.getAnimalCount() >= this.config.maxAnimals) { |
| this.showDebug('已达到最大动物数量,不再生成新动物'); |
| return; |
| } |
| |
| |
| if (!this.currentFood) { |
| this.showDebug('当前食物未设置,无法生成动物'); |
| return; |
| } |
| |
| const side = Math.random() < 0.5 ? 'left' : 'right'; |
| console.log(`尝试生成动物: 食物类型=${this.currentFood}, 边=${side}, 当前动物数=${this.animalSystem.getAnimalCount()}`); |
| |
| const animal = this.animalSystem.createAnimal(this.currentFood, side); |
| if (animal) { |
| |
| this.showDebug(`🐾 小动物出现了!类型: ${animal.type}, 食物: ${this.currentFood}, 边: ${side}, 总数: ${this.animalSystem.getAnimalCount()}`); |
| |
| |
| if (this.audio) { |
| this.audio.playAnimalAppear(); |
| } |
| } else { |
| this.showDebug('❌ 动物生成失败'); |
| } |
| } |
| |
| spawnLittleGirl() { |
| const girl = this.animalSystem.createLittleGirl('right'); |
| if (girl) { |
| |
| this.showDebug('小女孩出现了!'); |
| |
| |
| if (this.audio) { |
| this.audio.playGirlAppear(); |
| } |
| |
| |
| this.particleSystem.createSparkleParticles( |
| this.canvas.width - 100, |
| this.canvas.height - 100, |
| 10 |
| ); |
| } |
| } |
| |
| createCelebrationEffect() { |
| |
| if (this.audio) { |
| this.audio.playCelebration(); |
| } |
| |
| |
| for (let i = 0; i < 5; i++) { |
| setTimeout(() => { |
| const x = Math.random() * this.canvas.width; |
| const y = Math.random() * this.canvas.height * 0.5; |
| this.particleSystem.createExplosionParticles(x, y, 15, '#FFD700'); |
| this.particleSystem.createExplosionParticles(x, y, 15, '#FF69B4'); |
| }, i * 500); |
| } |
| } |
| |
| createBirthdayEffect() { |
| |
| const centerX = this.canvas.width / 2; |
| const centerY = this.canvas.height / 2; |
| |
| this.particleSystem.createSparkleParticles(centerX, centerY, 20); |
| this.showSpecialMessage('生日快乐!🎂'); |
| } |
| |
| showSpecialMessage(message) { |
| |
| const messageDiv = document.createElement('div'); |
| messageDiv.textContent = message; |
| messageDiv.style.cssText = ` |
| position: fixed; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| background: linear-gradient(45deg, #FF6B6B, #4ECDC4); |
| color: white; |
| padding: 20px 30px; |
| border-radius: 15px; |
| font-size: 18px; |
| font-weight: bold; |
| box-shadow: 0 8px 32px rgba(0,0,0,0.3); |
| z-index: 10000; |
| animation: specialMessage 3s ease-in-out forwards; |
| `; |
| |
| document.body.appendChild(messageDiv); |
| |
| setTimeout(() => { |
| if (messageDiv.parentNode) { |
| messageDiv.parentNode.removeChild(messageDiv); |
| } |
| }, 3000); |
| } |
| |
| render() { |
| |
| this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); |
| |
| |
| this.renderBackground(); |
| |
| |
| this.pot.render(this.ctx); |
| |
| |
| this.particleSystem.render(this.ctx); |
| |
| |
| this.animalSystem.render(this.ctx); |
| |
| |
| this.renderEffects(); |
| } |
| |
| renderBackground() { |
| |
| const gradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height); |
| gradient.addColorStop(0, '#87CEEB'); |
| gradient.addColorStop(1, '#98FB98'); |
| |
| this.ctx.fillStyle = gradient; |
| this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); |
| |
| |
| this.renderDecorations(); |
| } |
| |
| renderDecorations() { |
| |
| this.ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; |
| this.drawCloud(100, 80, 60); |
| this.drawCloud(300, 60, 40); |
| this.drawCloud(500, 90, 50); |
| } |
| |
| drawCloud(x, y, size) { |
| this.ctx.beginPath(); |
| this.ctx.arc(x, y, size * 0.5, 0, Math.PI * 2); |
| this.ctx.arc(x + size * 0.3, y, size * 0.6, 0, Math.PI * 2); |
| this.ctx.arc(x + size * 0.6, y, size * 0.4, 0, Math.PI * 2); |
| this.ctx.arc(x - size * 0.3, y, size * 0.4, 0, Math.PI * 2); |
| this.ctx.fill(); |
| } |
| |
| renderEffects() { |
| |
| if (this.gameState === 'activated' || this.gameState === 'cooking') { |
| this.renderMagicSparkles(); |
| } |
| } |
| |
| renderMagicSparkles() { |
| const time = Date.now() * 0.005; |
| this.ctx.fillStyle = '#FFD700'; |
| |
| for (let i = 0; i < 5; i++) { |
| const angle = (i / 5) * Math.PI * 2 + time; |
| const radius = 80 + Math.sin(time + i) * 20; |
| const x = this.pot.x + Math.cos(angle) * radius; |
| const y = this.pot.y + Math.sin(angle) * radius; |
| |
| this.ctx.beginPath(); |
| this.ctx.arc(x, y, 3, 0, Math.PI * 2); |
| this.ctx.fill(); |
| } |
| } |
| |
| updateUI() { |
| |
| const potStatus = document.getElementById('potStatus'); |
| const statusIndicator = potStatus.querySelector('.status-indicator'); |
| |
| statusIndicator.className = 'status-indicator'; |
| |
| switch (this.gameState) { |
| case 'idle': |
| statusIndicator.textContent = '魔法锅休眠中'; |
| statusIndicator.classList.add('inactive'); |
| break; |
| case 'activated': |
| statusIndicator.textContent = '魔法锅已激活'; |
| statusIndicator.classList.add('active'); |
| break; |
| case 'cooking': |
| statusIndicator.textContent = '正在烹饪中...'; |
| statusIndicator.classList.add('cooking'); |
| break; |
| case 'overflowing': |
| statusIndicator.textContent = '食物涌出中!'; |
| statusIndicator.classList.add('overflowing'); |
| break; |
| case 'stopped': |
| statusIndicator.textContent = '烹饪已停止'; |
| statusIndicator.classList.add('inactive'); |
| break; |
| } |
| |
| |
| const cookingInfo = document.getElementById('cookingInfo'); |
| const currentFoodSpan = document.getElementById('currentFood'); |
| |
| if (this.gameState === 'cooking' && this.currentFood) { |
| cookingInfo.style.display = 'block'; |
| currentFoodSpan.textContent = this.currentFood; |
| } else { |
| cookingInfo.style.display = 'none'; |
| } |
| |
| |
| const foodCounter = document.getElementById('foodCounter'); |
| const foodCount = document.getElementById('foodCount'); |
| const foodEmoji = document.getElementById('foodEmoji'); |
| |
| if (this.gameState === 'overflowing' || this.gameState === 'stopped') { |
| foodCounter.style.display = 'block'; |
| foodCount.textContent = this.foodCount; |
| foodEmoji.textContent = this.getFoodEmoji(this.currentFood); |
| } else { |
| foodCounter.style.display = 'none'; |
| } |
| |
| |
| this.updateVoiceHints(); |
| |
| |
| this.updatePerformanceDisplay(); |
| } |
| |
| updateVoiceHints() { |
| const foodHint = document.getElementById('foodHint'); |
| const stopHint = document.getElementById('stopHint'); |
| |
| foodHint.style.display = this.gameState === 'activated' ? 'block' : 'none'; |
| stopHint.style.display = this.gameState === 'overflowing' ? 'block' : 'none'; |
| } |
| |
| updatePerformanceDisplay() { |
| const performanceMonitor = document.getElementById('performanceMonitor'); |
| const particleCount = document.getElementById('particleCount'); |
| const animalCount = document.getElementById('animalCount'); |
| const fpsCount = document.getElementById('fpsCount'); |
| |
| if (performanceMonitor && particleCount && animalCount && fpsCount) { |
| const perfInfo = this.getPerformanceInfo(); |
| |
| particleCount.textContent = perfInfo.particles; |
| animalCount.textContent = perfInfo.animals; |
| fpsCount.textContent = perfInfo.fps; |
| |
| |
| const shouldShow = this.gameState === 'overflowing' || |
| this.gameState === 'cooking' || |
| perfInfo.particles > 50; |
| |
| performanceMonitor.style.display = shouldShow ? 'block' : 'none'; |
| |
| |
| if (perfInfo.particles > this.config.maxParticles * 0.8) { |
| particleCount.style.color = '#ff6b6b'; |
| } else { |
| particleCount.style.color = '#ffffff'; |
| } |
| |
| if (perfInfo.animals > this.config.maxAnimals * 0.8) { |
| animalCount.style.color = '#ff6b6b'; |
| } else { |
| animalCount.style.color = '#ffffff'; |
| } |
| |
| if (perfInfo.fps < 30) { |
| fpsCount.style.color = '#ff6b6b'; |
| } else if (perfInfo.fps < 45) { |
| fpsCount.style.color = '#f39c12'; |
| } else { |
| fpsCount.style.color = '#27ae60'; |
| } |
| } |
| } |
| |
| updateCookingProgress(progress) { |
| const progressFill = document.getElementById('progressFill'); |
| if (progressFill) { |
| progressFill.style.width = progress + '%'; |
| } |
| } |
| |
| updateVoiceStatus(status) { |
| const voiceStatus = document.getElementById('voiceStatus'); |
| const statusText = voiceStatus.querySelector('.status-text'); |
| |
| switch (status) { |
| case 'listening': |
| statusText.textContent = '正在听取...'; |
| voiceStatus.style.background = '#e74c3c'; |
| break; |
| case 'processing': |
| statusText.textContent = '处理中...'; |
| voiceStatus.style.background = '#f39c12'; |
| break; |
| case 'ready': |
| statusText.textContent = '准备就绪'; |
| voiceStatus.style.background = '#4ecdc4'; |
| break; |
| case 'error': |
| statusText.textContent = '语音错误'; |
| voiceStatus.style.background = '#95a5a6'; |
| break; |
| } |
| |
| |
| this.updateVoiceActivateButton(); |
| } |
| |
| getFoodEmoji(foodName) { |
| const emojiMap = { |
| 'apple': '🍎', |
| 'banana': '🍌', |
| 'orange': '🍊', |
| 'strawberry': '🍓', |
| 'watermelon': '🍉', |
| 'grape': '🍇', |
| 'pizza': '🍕', |
| 'burger': '🍔', |
| 'cake': '🍰', |
| 'cookie': '🍪', |
| 'bread': '🍞', |
| 'cheese': '🧀' |
| }; |
| |
| return emojiMap[foodName] || '🍽️'; |
| } |
| |
| showDebug(message) { |
| const debugLog = document.getElementById('debugLog'); |
| if (debugLog) { |
| const time = new Date().toLocaleTimeString(); |
| const perfInfo = this.getPerformanceInfo(); |
| const perfText = `FPS:${perfInfo.fps} P:${perfInfo.particles} A:${perfInfo.animals} M:${perfInfo.memoryUsage}KB`; |
| debugLog.innerHTML += `<div>[${time}] [${perfText}] ${message}</div>`; |
| debugLog.scrollTop = debugLog.scrollHeight; |
| |
| |
| const lines = debugLog.children; |
| if (lines.length > 50) { |
| for (let i = 0; i < 10; i++) { |
| debugLog.removeChild(lines[0]); |
| } |
| } |
| } |
| console.log(`[MagicPot] ${message}`); |
| } |
| |
| toggleDebug() { |
| const debugInfo = document.getElementById('debugInfo'); |
| debugInfo.style.display = debugInfo.style.display === 'none' ? 'block' : 'none'; |
| } |
| |
| resizeCanvas() { |
| this.setupCanvas(); |
| } |
| |
| showVoiceHelp() { |
| const helpContent = ` |
| <h3>🎤 语音命令帮助</h3> |
| <div class="help-section"> |
| <h4>🪄 激活魔法锅</h4> |
| <p>说出:"<strong>cook cook cook pot</strong>"</p> |
| <p>其他可用命令:</p> |
| <ul> |
| <li>"magic pot cook"</li> |
| <li>"cook three times pot"</li> |
| </ul> |
| </div> |
| |
| <div class="help-section"> |
| <h4>🍎 开始烹饪</h4> |
| <p>魔法锅激活后,说出任何食物名称:</p> |
| <ul> |
| <li><strong>水果:</strong>apple, banana, orange, strawberry, watermelon, grape</li> |
| <li><strong>主食:</strong>pizza, burger, bread, rice, noodles</li> |
| <li><strong>甜点:</strong>cake, cookie</li> |
| <li><strong>其他:</strong>cheese, fish, chicken</li> |
| </ul> |
| </div> |
| |
| <div class="help-section"> |
| <h4>🛑 停止烹饪</h4> |
| <p>说出:"<strong>stop stop stop pot</strong>"</p> |
| <p>其他可用命令:</p> |
| <ul> |
| <li>"magic pot stop"</li> |
| <li>"stop three times pot"</li> |
| </ul> |
| </div> |
| |
| <div class="help-section"> |
| <h4>🎁 特殊彩蛋</h4> |
| <ul> |
| <li><strong>草莓/西瓜:</strong>数量达到30个时,小女孩会出现</li> |
| <li><strong>生日蛋糕:</strong>制作蛋糕时有特殊庆祝效果</li> |
| <li><strong>百食庆祝:</strong>制作100个食物时有烟花效果</li> |
| </ul> |
| </div> |
| |
| <div class="help-section"> |
| <h4>🔊 音效控制</h4> |
| <ul> |
| <li><strong>音效按钮:</strong>点击🔊按钮开关音效</li> |
| <li><strong>F10:</strong>快捷键切换音效开关</li> |
| <li><strong>F11:</strong>测试所有音效</li> |
| </ul> |
| </div> |
| |
| <div class="help-section"> |
| <h4>🎵 音效说明</h4> |
| <ul> |
| <li><strong>激活音效:</strong>魔法锅激活时的神奇音调</li> |
| <li><strong>烹饪音效:</strong>烹饪过程中的冒泡声</li> |
| <li><strong>食物弹出:</strong>食物涌出时的弹跳声</li> |
| <li><strong>动物出现:</strong>小动物出现时的鸟叫声</li> |
| <li><strong>小女孩:</strong>小女孩出现时的铃声</li> |
| <li><strong>庆祝音效:</strong>达成成就时的号角声</li> |
| </ul> |
| </div> |
| |
| <div class="help-section"> |
| <h4>💡 使用技巧</h4> |
| <ul> |
| <li>说话清晰,语速适中</li> |
| <li>确保浏览器已允许麦克风权限</li> |
| <li>在安静的环境中使用效果更佳</li> |
| <li>按F12可查看调试信息</li> |
| <li><strong>按F9激活/取消激活键盘面板</strong></li> |
| <li><strong>按F8显示/隐藏键盘面板</strong></li> |
| <li><strong>按F7清空命令历史</strong></li> |
| <li><strong>按F5重置面板位置</strong></li> |
| <li><strong>按F4调整食物生成速度</strong></li> |
| <li><strong>按Alt+A强制生成动物(调试)</strong></li> |
| <li>首次使用时点击任意按钮激活音频</li> |
| </ul> |
| </div> |
| |
| <div class="help-section"> |
| <h4>⌨️ 键盘输入调试</h4> |
| <ul> |
| <li><strong>F9:</strong> 激活/取消激活键盘面板 ⭐</li> |
| <li><strong>Ctrl+K:</strong> 聚焦到输入框(自动激活)</li> |
| <li><strong>Ctrl+反引号:</strong> 切换输入面板</li> |
| <li><strong>数字键1-6:</strong> 快速执行常用命令</li> |
| <li><strong>↑↓方向键:</strong> 浏览命令历史</li> |
| <li><strong>Enter:</strong> 执行当前输入的命令</li> |
| <li><strong>Esc:</strong> 清空输入或取消激活</li> |
| </ul> |
| <p style="color: #f39c12; font-style: italic;"> |
| 💡 面板默认隐藏,按F9激活并显示面板后才能输入命令<br> |
| 🖱️ 激活后可拖拽标题栏移动面板,按F5重置到右下角<br> |
| 👁️ 按F8可手动显示/隐藏面板,取消激活时自动隐藏 |
| </p> |
| </div> |
| `; |
| |
| this.showModal('语音命令帮助', helpContent); |
| } |
| |
| showModal(title, content) { |
| |
| const modal = document.createElement('div'); |
| modal.className = 'help-modal'; |
| modal.innerHTML = ` |
| <div class="modal-overlay"></div> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h2>${title}</h2> |
| <button class="modal-close">✕</button> |
| </div> |
| <div class="modal-body"> |
| ${content} |
| </div> |
| </div> |
| `; |
| |
| document.body.appendChild(modal); |
| |
| |
| const closeBtn = modal.querySelector('.modal-close'); |
| const overlay = modal.querySelector('.modal-overlay'); |
| |
| const closeModal = () => { |
| modal.style.animation = 'modalFadeOut 0.3s ease-out forwards'; |
| setTimeout(() => { |
| if (modal.parentNode) { |
| modal.parentNode.removeChild(modal); |
| } |
| }, 300); |
| }; |
| |
| closeBtn.addEventListener('click', closeModal); |
| overlay.addEventListener('click', closeModal); |
| |
| |
| const handleKeydown = (e) => { |
| if (e.key === 'Escape') { |
| closeModal(); |
| document.removeEventListener('keydown', handleKeydown); |
| } |
| }; |
| document.addEventListener('keydown', handleKeydown); |
| } |
| |
| |
| startPerformanceMonitoring() { |
| this.showDebug('性能监控已启动'); |
| } |
| |
| updatePerformanceStats(currentTime) { |
| |
| this.performanceStats.frameCount++; |
| if (currentTime - this.performanceStats.lastFpsTime >= 1000) { |
| this.performanceStats.fps = this.performanceStats.frameCount; |
| this.performanceStats.frameCount = 0; |
| this.performanceStats.lastFpsTime = currentTime; |
| } |
| |
| |
| this.performanceStats.particles = this.particleSystem.getParticleCount(); |
| this.performanceStats.animals = this.animalSystem.getAnimalCount(); |
| } |
| |
| performCleanupIfNeeded(currentTime) { |
| |
| if (currentTime - this.lastCleanupTime >= this.config.cleanupInterval) { |
| this.performMemoryCleanup(); |
| this.lastCleanupTime = currentTime; |
| } |
| } |
| |
| performMemoryCleanup() { |
| const beforeParticles = this.particleSystem.getParticleCount(); |
| const beforeAnimals = this.animalSystem.getAnimalCount(); |
| |
| |
| this.cleanupGroundedParticles(); |
| |
| |
| this.cleanupOldAnimals(); |
| |
| |
| this.particleSystem.cleanupInvalidParticles(); |
| |
| const afterParticles = this.particleSystem.getParticleCount(); |
| const afterAnimals = this.animalSystem.getAnimalCount(); |
| |
| if (beforeParticles !== afterParticles || beforeAnimals !== afterAnimals) { |
| this.showDebug(`内存清理完成 - 粒子: ${beforeParticles}→${afterParticles}, 动物: ${beforeAnimals}→${afterAnimals}`); |
| } |
| } |
| |
| cleanupGroundedParticles() { |
| const groundedParticles = this.particleSystem.particles.filter(p => |
| p.type === 'food' && p.isGrounded |
| ); |
| |
| if (groundedParticles.length > this.config.maxGroundedParticles) { |
| |
| const toRemove = groundedParticles.length - this.config.maxGroundedParticles; |
| const oldestParticles = groundedParticles |
| .sort((a, b) => a.lifeTime - b.lifeTime) |
| .slice(0, toRemove); |
| |
| oldestParticles.forEach(particle => { |
| const index = this.particleSystem.particles.indexOf(particle); |
| if (index > -1) { |
| this.particleSystem.particles.splice(index, 1); |
| } |
| }); |
| } |
| } |
| |
| cleanupOldAnimals() { |
| |
| const oldAnimals = this.animalSystem.animals.filter(animal => |
| animal.lifeTime > 45 && animal.state !== 'eating' |
| ); |
| |
| oldAnimals.forEach(animal => { |
| animal.shouldRemove = true; |
| }); |
| } |
| |
| |
| getPerformanceInfo() { |
| return { |
| fps: this.performanceStats.fps, |
| particles: this.performanceStats.particles, |
| animals: this.performanceStats.animals, |
| foodCount: this.foodCount, |
| gameState: this.gameState, |
| memoryUsage: this.estimateMemoryUsage() |
| }; |
| } |
| |
| estimateMemoryUsage() { |
| |
| const particleMemory = this.performanceStats.particles * 0.5; |
| const animalMemory = this.performanceStats.animals * 1; |
| const baseMemory = 100; |
| |
| return Math.round(baseMemory + particleMemory + animalMemory); |
| } |
| |
| |
| forceSpawnAnimal() { |
| console.log('=== 强制生成动物调试 ==='); |
| console.log(`当前食物: ${this.currentFood}`); |
| console.log(`食物数量: ${this.foodCount}`); |
| console.log(`当前动物数: ${this.animalSystem.getAnimalCount()}`); |
| console.log(`最大动物数: ${this.config.maxAnimals}`); |
| console.log(`游戏状态: ${this.gameState}`); |
| console.log(`Canvas尺寸: ${this.canvas.width}x${this.canvas.height}`); |
| |
| if (!this.currentFood) { |
| this.showDebug('❌ 当前没有食物类型,无法生成动物'); |
| return; |
| } |
| |
| if (this.animalSystem.getAnimalCount() >= this.config.maxAnimals) { |
| this.showDebug(`❌ 已达到最大动物数量 ${this.config.maxAnimals},清理一些动物后再试`); |
| |
| this.animalSystem.makeAllAnimalsLeave(); |
| return; |
| } |
| |
| this.showDebug('🔧 强制尝试生成动物...'); |
| this.spawnAnimal(); |
| |
| |
| setTimeout(() => { |
| console.log('=== 当前动物状态 ==='); |
| this.animalSystem.animals.forEach((animal, index) => { |
| console.log(`动物${index + 1}: ${animal.type}, 位置(${animal.x}, ${animal.y}), 状态: ${animal.state}, emoji: ${animal.emoji}`); |
| }); |
| }, 100); |
| } |
| |
| |
| toggleAudio() { |
| if (this.audio) { |
| const newState = !this.audio.isEnabled; |
| this.audio.setEnabled(newState); |
| this.updateAudioButton(); |
| |
| if (newState) { |
| this.showDebug('音效已开启'); |
| |
| setTimeout(() => { |
| this.audio.playPotActivate(); |
| }, 100); |
| } else { |
| this.showDebug('音效已关闭'); |
| } |
| } |
| } |
| |
| updateAudioButton() { |
| const audioButton = document.getElementById('audioButton'); |
| if (audioButton && this.audio) { |
| if (this.audio.isEnabled) { |
| audioButton.textContent = '🔊'; |
| audioButton.classList.remove('muted'); |
| audioButton.title = '点击关闭音效'; |
| } else { |
| audioButton.textContent = '🔇'; |
| audioButton.classList.add('muted'); |
| audioButton.title = '点击开启音效'; |
| } |
| } |
| } |
| |
| updateVoiceActivateButton() { |
| const voiceActivateButton = document.getElementById('voiceActivateButton'); |
| if (voiceActivateButton && this.voice) { |
| const status = this.voice.getStatus(); |
| |
| |
| voiceActivateButton.classList.remove('active', 'disabled'); |
| |
| if (!status.isSupported) { |
| voiceActivateButton.classList.add('disabled'); |
| voiceActivateButton.title = '浏览器不支持语音识别'; |
| voiceActivateButton.textContent = '❌'; |
| } else if (status.isDisabled) { |
| voiceActivateButton.classList.add('disabled'); |
| voiceActivateButton.title = '语音识别已禁用,点击重新激活'; |
| voiceActivateButton.textContent = '🔇'; |
| } else if (status.isListening) { |
| voiceActivateButton.classList.add('active'); |
| voiceActivateButton.title = '语音识别运行中'; |
| voiceActivateButton.textContent = '🎤'; |
| } else { |
| voiceActivateButton.title = '点击激活语音识别'; |
| voiceActivateButton.textContent = '🎤'; |
| } |
| } |
| } |
| |
| |
| testAudio() { |
| if (this.audio) { |
| this.showDebug('开始音效测试...'); |
| this.audio.testAllSounds(); |
| } |
| } |
| |
| |
| restartVoice() { |
| if (this.voice) { |
| this.showDebug('手动重启语音识别系统'); |
| this.voice.restart(); |
| } |
| } |
| |
| |
| manualActivateVoice() { |
| if (this.voice) { |
| this.showDebug('手动激活语音识别'); |
| this.voice.manualActivate(); |
| } |
| } |
| |
| |
| toggleKeyboardPanel() { |
| if (this.keyboard) { |
| this.keyboard.togglePanel(); |
| const stats = this.keyboard.getStats(); |
| this.showDebug(`键盘面板${stats.isCollapsed ? '已折叠' : '已展开'}`); |
| } |
| } |
| |
| |
| clearCommandHistory() { |
| if (this.keyboard) { |
| this.keyboard.clearHistory(); |
| this.showDebug('键盘命令历史已清空'); |
| } |
| } |
| |
| |
| toggleKeyboardActivation() { |
| if (this.keyboard) { |
| console.log('F9 pressed - toggling keyboard activation'); |
| this.keyboard.toggleActiveState(); |
| const stats = this.keyboard.getStats(); |
| |
| |
| if (stats.isActive) { |
| this.keyboard.setPanelCollapsed(false); |
| this.showDebug('键盘面板已激活并显示'); |
| } else { |
| this.keyboard.setPanelCollapsed(true); |
| this.showDebug('键盘面板已取消激活并隐藏'); |
| } |
| |
| console.log('Keyboard activation state:', stats.isActive); |
| } else { |
| console.log('Keyboard handler not initialized'); |
| } |
| } |
| |
| |
| resetKeyboardPanelPosition() { |
| if (this.keyboard) { |
| this.keyboard.resetPosition(); |
| this.showDebug('键盘面板位置已重置'); |
| } |
| } |
| |
| |
| adjustFoodSpawnSpeed() { |
| |
| const speeds = [ |
| { interval: 800, rate: 1, name: '慢速' }, |
| { interval: 400, rate: 1, name: '中速' }, |
| { interval: 200, rate: 2, name: '快速' }, |
| { interval: 100, rate: 3, name: '超快' } |
| ]; |
| |
| |
| let currentIndex = speeds.findIndex(speed => speed.interval === this.foodSpawnInterval); |
| if (currentIndex === -1) currentIndex = 0; |
| |
| |
| const nextIndex = (currentIndex + 1) % speeds.length; |
| const newSpeed = speeds[nextIndex]; |
| |
| this.foodSpawnInterval = newSpeed.interval; |
| this.config.particleSpawnRate = newSpeed.rate; |
| |
| this.showDebug(`食物生成速度已调整为: ${newSpeed.name} (${newSpeed.interval}ms间隔)`); |
| this.showSpecialMessage(`🍽️ 食物生成速度: ${newSpeed.name}`); |
| } |
| |
| |
| getDebugInfo() { |
| const info = { |
| game: this.getPerformanceInfo(), |
| voice: this.voice ? this.voice.getStatus() : null, |
| keyboard: this.keyboard ? this.keyboard.getStats() : null |
| }; |
| |
| return info; |
| } |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| window.game = new MagicPotGame(); |
| }); |
|
|