import { SCREEN_WIDTH, SCREEN_HEIGHT } from './render'; import AudioRecorder from './audioRecorder'; import AIOpponent from './ai'; import storage from './storage'; const ctx = canvas.getContext('2d'); const DOGS = [ { id: 'shiba', name: '柴犬', desc: '平衡型', image: 'images/wangwang/dogs/shiba.png', thumb: 'images/wangwang/dogs/shiba.png', unlockWP: 0 }, { id: 'pomeranian', name: '博美', desc: '连击加成', image: 'images/wangwang/dogs/pomeranian_bark.png', thumb: 'images/wangwang/dogs/pomeranian_idle.png', unlockWP: 0 }, { id: 'husky', name: '哈士奇', desc: '音量需求-', image: 'images/wangwang/dogs/husky.png', thumb: 'images/wangwang/dogs/husky.png', unlockWP: 200 }, { id: 'corgi', name: '柯基', desc: 'K.O.伤害+', image: 'images/wangwang/dogs/corgi_bark.png', thumb: 'images/wangwang/dogs/corgi_idle.png', unlockWP: 400 }, { id: 'golden', name: '金毛', desc: '音量加成+', image: 'images/wangwang/dogs/golden.png', thumb: 'images/wangwang/dogs/golden.png', unlockWP: 600 } ]; const ASSETS = { bg: 'images/wangwang/ui/bg.jpg', start: 'images/wangwang/ui/start.jpg', back: 'images/wangwang/ui/back.jpg', victory: 'images/wangwang/ui/victory.jpg', defeat: 'images/wangwang/ui/defeat.jpg', hpPlayer: 'images/wangwang/ui/hp_player.jpg', hpEnemy: 'images/wangwang/ui/hp_enemy.jpg', coin: 'images/wangwang/ui/coin.jpg', lock: 'images/wangwang/ui/lock.jpg' }; export default class Main { state = 'MENU'; // MENU | COUNTDOWN | BATTLE | RESULT images = {}; energy = 50; combo = 0; volume = 0; ai = null; recorder = null; aniId = 0; lastTime = 0; countdown = 3; countdownTimer = null; selectedDog = 'shiba'; unlocked = ['shiba', 'pomeranian']; wp = 0; isWin = false; rewardWP = 0; touchAreas = []; constructor() { this.wp = storage.getWP(); this.unlocked = storage.getUnlocked(); this.selectedDog = storage.getSelectedDog(); this.loadImages().then(() => { this.bindTouch(); this.renderMenu(); }); } loadImages() { const allSources = { ...ASSETS }; DOGS.forEach(d => { allSources[`dog_${d.id}`] = d.image; allSources[`thumb_${d.id}`] = d.thumb; }); const promises = Object.entries(allSources).map(([key, src]) => { return new Promise((resolve) => { const img = wx.createImage(); img.onload = () => { this.images[key] = img; resolve(); }; img.onerror = () => { console.error('load image failed', src); resolve(); }; img.src = src; }); }); return Promise.all(promises); } bindTouch() { wx.onTouchStart((e) => { const touch = e.touches[0]; this.handleTouch(touch.clientX, touch.clientY); }); } handleTouch(x, y) { for (const area of this.touchAreas) { if ( x >= area.x && x <= area.x + area.w && y >= area.y && y <= area.y + area.h ) { area.action(); return; } } } // ===== MENU ===== renderMenu() { this.state = 'MENU'; this.touchAreas = []; this.drawMenu(); } drawMenu() { const w = SCREEN_WIDTH; const h = SCREEN_HEIGHT; ctx.clearRect(0, 0, w, h); if (this.images.bg) { ctx.drawImage(this.images.bg, 0, 0, w, h); } else { ctx.fillStyle = '#FFD97A'; ctx.fillRect(0, 0, w, h); } // 标题 ctx.fillStyle = '#8B4513'; ctx.font = 'bold 36px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('汪汪大作战', w / 2, 80); ctx.font = '14px sans-serif'; ctx.fillText('对着麦克风喊"汪"来战斗!', w / 2, 105); // WP 显示 this.drawWP(w - 80, 40); // 当前选中狗展示 const dog = DOGS.find(d => d.id === this.selectedDog); if (dog && this.images[`dog_${dog.id}`]) { const dw = 180; const dh = 180; ctx.drawImage(this.images[`dog_${dog.id}`], (w - dw) / 2, 140, dw, dh); } if (dog) { ctx.fillStyle = '#5D4037'; ctx.font = 'bold 22px sans-serif'; ctx.fillText(dog.name, w / 2, 340); ctx.font = '14px sans-serif'; ctx.fillText(dog.desc, w / 2, 365); } // 狗狗选择栏 const itemW = 70; const itemH = 90; const gap = 15; const totalW = DOGS.length * itemW + (DOGS.length - 1) * gap; const startX = (w - totalW) / 2; const listY = 400; DOGS.forEach((d, i) => { const x = startX + i * (itemW + gap); const y = listY; const isSelected = d.id === this.selectedDog; const isUnlocked = this.unlocked.includes(d.id) || d.unlockWP === 0; ctx.fillStyle = isSelected ? 'rgba(255, 200, 150, 0.9)' : 'rgba(255, 255, 255, 0.8)'; ctx.strokeStyle = isSelected ? '#FF6B00' : 'transparent'; ctx.lineWidth = 3; this.roundRect(x, y, itemW, itemH, 10); ctx.fill(); ctx.stroke(); if (this.images[`thumb_${d.id}`]) { ctx.save(); if (!isUnlocked) ctx.globalAlpha = 0.5; ctx.drawImage(this.images[`thumb_${d.id}`], x + 5, y + 5, itemW - 10, itemH - 35); ctx.restore(); } if (!isUnlocked && this.images.lock) { ctx.drawImage(this.images.lock, x + itemW - 22, y + 4, 18, 18); } ctx.fillStyle = '#5D4037'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(d.name, x + itemW / 2, y + itemH - 10); this.touchAreas.push({ x, y, w: itemW, h: itemH, action: () => this.onSelectDog(d) }); }); // 开始按钮 if (this.images.start) { const bw = 220; const bh = 75; const bx = (w - bw) / 2; const by = 540; ctx.drawImage(this.images.start, bx, by, bw, bh); this.touchAreas.push({ x: bx, y: by, w: bw, h: bh, action: () => this.startCountdown() }); } } drawWP(x, y) { if (this.images.coin) { ctx.drawImage(this.images.coin, x - 20, y - 12, 24, 24); } ctx.fillStyle = '#D35400'; ctx.font = 'bold 16px sans-serif'; ctx.textAlign = 'left'; ctx.fillText('WP: ' + this.wp, x + 8, y + 6); } onSelectDog(dog) { if (!this.unlocked.includes(dog.id) && dog.unlockWP > 0) { if (this.wp >= dog.unlockWP) { this.wp = storage.addWP(-dog.unlockWP); this.unlocked = storage.unlockDog(dog.id); this.selectedDog = dog.id; storage.setSelectedDog(dog.id); } } else { this.selectedDog = dog.id; storage.setSelectedDog(dog.id); } this.drawMenu(); } // ===== COUNTDOWN ===== startCountdown() { this.state = 'COUNTDOWN'; this.countdown = 3; this.touchAreas = []; this.countdownTimer = setInterval(() => { this.countdown--; if (this.countdown <= 0) { clearInterval(this.countdownTimer); this.startBattle(); } else { this.drawCountdown(); } }, 1000); this.drawCountdown(); } drawCountdown() { const w = SCREEN_WIDTH; const h = SCREEN_HEIGHT; ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.fillRect(0, 0, w, h); ctx.fillStyle = '#FFD700'; ctx.font = 'bold 120px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(this.countdown === 0 ? 'GO!' : String(this.countdown), w / 2, h / 2); ctx.textBaseline = 'alphabetic'; } // ===== BATTLE ===== startBattle() { this.state = 'BATTLE'; this.energy = 50; this.combo = 0; this.volume = 0; this.ai = new AIOpponent('normal'); this.ai.reset(); this.recorder = new AudioRecorder({ sampleRate: 16000, onUpdate: (status) => { this.volume = status.volume; this.combo = status.combo; if (status.isBark && this.state === 'BATTLE') { this.energy = Math.min(100, this.energy + status.pushPower * 100); try { if (wx.vibrateShort) wx.vibrateShort({ type: 'light' }); } catch (e) {} } }, onError: (err) => { console.error('recorder error', err); wx.showToast({ title: err.message || '麦克风权限失败', icon: 'none', duration: 3000 }); } }); this.recorder.start(); this.lastTime = Date.now(); this.aniId = requestAnimationFrame(this.loop.bind(this)); } loop() { if (this.state !== 'BATTLE') return; const now = Date.now(); const dt = Math.min((now - this.lastTime) / 1000, 0.1); this.lastTime = now; const aiAction = this.ai.update(dt); if (aiAction && aiAction.type === 'bark') { this.energy = Math.max(0, this.energy - aiAction.power * 100); } if (this.energy >= 100) { this.endBattle(true); return; } if (this.energy <= 0) { this.endBattle(false); return; } this.drawBattle(); this.aniId = requestAnimationFrame(this.loop.bind(this)); } drawBattle() { const w = SCREEN_WIDTH; const h = SCREEN_HEIGHT; ctx.clearRect(0, 0, w, h); if (this.images.bg) { ctx.drawImage(this.images.bg, 0, 0, w, h); } else { ctx.fillStyle = '#FFD97A'; ctx.fillRect(0, 0, w, h); } // 返回按钮 if (this.images.back) { ctx.drawImage(this.images.back, 10, 10, 70, 24); this.touchAreas = [{ x: 10, y: 10, w: 70, h: 24, action: () => this.backToMenu() }]; } // 能量条 const barY = h * 0.55; const barW = w * 0.8; const barH = 24; const barX = (w - barW) / 2; ctx.fillStyle = 'rgba(0,0,0,0.3)'; this.roundRect(barX, barY, barW, barH, 12); ctx.fill(); const fillW = (this.energy / 100) * barW; const gradient = ctx.createLinearGradient(barX, barY, barX + fillW, barY); gradient.addColorStop(0, '#E74C3C'); gradient.addColorStop(0.5, '#F39C12'); gradient.addColorStop(1, '#3498DB'); ctx.fillStyle = gradient; this.roundRect(barX, barY, fillW, barH, 12); ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; this.roundRect(barX, barY, barW, barH, 12); ctx.stroke(); ctx.fillStyle = '#fff'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(Math.round(this.energy) + '%', barX + fillW, barY - 8); // 玩家狗 const playerImg = this.images[`dog_${this.selectedDog}`]; if (playerImg) { ctx.drawImage(playerImg, w * 0.12, h * 0.28, 140, 140); } // 敌方狗 const enemyImg = this.images.dog_pomeranian; if (enemyImg) { ctx.save(); ctx.translate(w * 0.88 + 140, h * 0.28); ctx.scale(-1, 1); ctx.drawImage(enemyImg, 0, 0, 140, 140); ctx.restore(); } // 标签 ctx.fillStyle = '#5D4037'; ctx.font = 'bold 14px sans-serif'; ctx.fillText('我方', w * 0.12 + 70, h * 0.28 + 160); ctx.fillText('敌方', w * 0.88 - 70, h * 0.28 + 160); // 音量条 const volH = Math.min(this.volume * 100, 80); ctx.fillStyle = `rgba(46, 204, 113, ${0.3 + this.volume * 0.7})`; ctx.fillRect(20, h - 100, 16, -volH); ctx.strokeStyle = '#27AE60'; ctx.lineWidth = 2; ctx.strokeRect(20, h - 100, 16, 80); // 实时音量显示 ctx.fillStyle = '#E74C3C'; ctx.font = 'bold 14px monospace'; ctx.textAlign = 'left'; ctx.fillText('VOL:' + this.volume.toFixed(2), 50, h - 20); if (this.combo > 1) { ctx.fillStyle = '#FF6B00'; ctx.font = 'bold 22px sans-serif'; ctx.textAlign = 'left'; ctx.fillText('连击 x' + this.combo, 50, h - 60); } // 提示 ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('对着麦克风喊 "汪"!', w / 2, h - 30); } // ===== RESULT ===== endBattle(isWin) { this.state = 'RESULT'; this.isWin = isWin; cancelAnimationFrame(this.aniId); if (this.recorder) { this.recorder.stop(); this.recorder = null; } this.rewardWP = isWin ? (this.combo >= 3 ? 50 : 30) : 5; this.wp = storage.addWP(this.rewardWP); if (isWin) storage.addWin(); this.drawResult(); } drawResult() { const w = SCREEN_WIDTH; const h = SCREEN_HEIGHT; ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(0, 0, w, h); const img = this.isWin ? this.images.victory : this.images.defeat; if (img) { const rw = w * 0.7; const rh = rw * (img.height / img.width); ctx.drawImage(img, (w - rw) / 2, h * 0.15, rw, rh); } ctx.fillStyle = '#FFD700'; ctx.font = 'bold 36px sans-serif'; ctx.textAlign = 'center'; ctx.fillText((this.isWin ? '+' : '') + this.rewardWP + ' WP', w / 2, h * 0.55); // 按钮 this.touchAreas = []; this.drawButton(w / 2 - 110, h * 0.65, 220, 50, '再来一局', '#FF6B00', () => this.startCountdown()); this.drawButton(w / 2 - 110, h * 0.75, 220, 50, '返回主页', '#8D6E63', () => this.renderMenu()); } drawButton(x, y, w, h, text, color, action) { ctx.fillStyle = color; this.roundRect(x, y, w, h, h / 2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.font = 'bold 18px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(text, x + w / 2, y + h / 2); ctx.textBaseline = 'alphabetic'; this.touchAreas.push({ x, y, w, h, action }); } backToMenu() { if (this.recorder) { this.recorder.stop(); this.recorder = null; } cancelAnimationFrame(this.aniId); this.renderMenu(); } roundRect(x, y, w, h, r) { const radius = Math.min(r, w / 2, h / 2); ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + w - radius, y); ctx.arcTo(x + w, y, x + w, y + radius, radius); ctx.lineTo(x + w, y + h - radius); ctx.arcTo(x + w, y + h, x + w - radius, y + h, radius); ctx.lineTo(x + radius, y + h); ctx.arcTo(x, y + h, x, y + h - radius, radius); ctx.lineTo(x, y + radius); ctx.arcTo(x, y, x + radius, y, radius); ctx.closePath(); } }