506 lines
14 KiB
JavaScript
506 lines
14 KiB
JavaScript
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();
|
||
}
|
||
}
|