wangwang/js/main.js

506 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}