343 lines
11 KiB
JavaScript
343 lines
11 KiB
JavaScript
/**
|
|
* MenuScene.js
|
|
* Main menu scene - displays game title and mode selection buttons.
|
|
* Rendered entirely with Canvas API (no DOM).
|
|
*/
|
|
|
|
const {
|
|
SCREEN_WIDTH,
|
|
SCREEN_HEIGHT,
|
|
COLORS,
|
|
SCENE,
|
|
GAME_MODE,
|
|
} = require('../base/GameGlobal');
|
|
const { t } = require('../i18n/I18n');
|
|
|
|
// ============================================================
|
|
// Button Layout
|
|
// ============================================================
|
|
const BTN_WIDTH = Math.min(SCREEN_WIDTH * 0.55, 280);
|
|
const BTN_HEIGHT = Math.min(36, SCREEN_HEIGHT * 0.07);
|
|
const BTN_GAP = Math.min(8, SCREEN_HEIGHT * 0.015);
|
|
const BTN_START_Y = SCREEN_HEIGHT * 0.38;
|
|
const BTN_X = (SCREEN_WIDTH - BTN_WIDTH) / 2;
|
|
|
|
// Half-width buttons for the utility row
|
|
const HALF_BTN_WIDTH = (BTN_WIDTH - BTN_GAP) / 2;
|
|
// Third-width buttons for 3-column row
|
|
const THIRD_BTN_WIDTH = (BTN_WIDTH - BTN_GAP * 2) / 3;
|
|
|
|
// Main game mode buttons (full width, vertical)
|
|
const MAIN_BUTTONS = [
|
|
{ labelKey: 'menu.classic', mode: GAME_MODE.CLASSIC, scene: SCENE.GAME },
|
|
{ labelKey: 'menu.endless', mode: GAME_MODE.ENDLESS, scene: SCENE.GAME },
|
|
{ labelKey: 'menu.pvp', mode: GAME_MODE.PVP, scene: SCENE.PVP_ROOM },
|
|
{ labelKey: 'menu.team3v3', mode: GAME_MODE.TEAM_3V3, scene: SCENE.TEAM_ROOM },
|
|
];
|
|
|
|
// Utility buttons: shop, daily gold, skin, ranking, settings (grid)
|
|
const UTIL_BUTTONS = [
|
|
{ labelKey: 'menu.shop', mode: null, scene: SCENE.SHOP },
|
|
{ labelKey: 'dailyGold.btn', mode: null, scene: 'DAILY_GOLD' },
|
|
{ labelKey: 'menu.skin', mode: null, scene: SCENE.SKIN },
|
|
{ labelKey: 'menu.ranking', mode: null, scene: SCENE.RANKING },
|
|
{ labelKey: 'menu.settings', mode: null, scene: SCENE.SETTINGS },
|
|
];
|
|
|
|
// Pre-calculate button rects for main buttons
|
|
const mainBtnRects = MAIN_BUTTONS.map((btn, i) => ({
|
|
x: BTN_X,
|
|
y: BTN_START_Y + i * (BTN_HEIGHT + BTN_GAP),
|
|
w: BTN_WIDTH,
|
|
h: BTN_HEIGHT,
|
|
...btn,
|
|
}));
|
|
|
|
// Pre-calculate button rects for utility buttons (row1: 3 cols, row2: 2 cols centered)
|
|
const utilStartY = BTN_START_Y + MAIN_BUTTONS.length * (BTN_HEIGHT + BTN_GAP) + BTN_GAP;
|
|
const utilBtnRects = UTIL_BUTTONS.map((btn, i) => {
|
|
if (i < 3) {
|
|
// First row: 3 buttons
|
|
return {
|
|
x: BTN_X + i * (THIRD_BTN_WIDTH + BTN_GAP),
|
|
y: utilStartY,
|
|
w: THIRD_BTN_WIDTH,
|
|
h: BTN_HEIGHT,
|
|
...btn,
|
|
};
|
|
} else {
|
|
// Second row: 2 buttons centered
|
|
const col = i - 3;
|
|
return {
|
|
x: BTN_X + col * (HALF_BTN_WIDTH + BTN_GAP),
|
|
y: utilStartY + (BTN_HEIGHT + BTN_GAP),
|
|
w: HALF_BTN_WIDTH,
|
|
h: BTN_HEIGHT,
|
|
...btn,
|
|
};
|
|
}
|
|
});
|
|
|
|
// Combined list for unified iteration
|
|
const buttonRects = [...mainBtnRects, ...utilBtnRects];
|
|
|
|
// ============================================================
|
|
// Menu Scene
|
|
// ============================================================
|
|
const MenuScene = {
|
|
_pressedIndex: -1,
|
|
_tankAnim: 0, // simple animation timer
|
|
|
|
enter() {
|
|
this._pressedIndex = -1;
|
|
this._tankAnim = 0;
|
|
|
|
// Auto-navigate to team room if there's a pending invite teamId
|
|
if (GameGlobal._pendingTeamId) {
|
|
const teamId = GameGlobal._pendingTeamId;
|
|
GameGlobal._pendingTeamId = null;
|
|
console.log(`[MenuScene] Found pendingTeamId: ${teamId}, will auto-navigate to TeamRoomScene`);
|
|
// Use setTimeout to allow the scene to fully initialize first
|
|
setTimeout(() => {
|
|
console.log(`[MenuScene] Auto-navigating to TeamRoomScene with teamId: ${teamId}`);
|
|
const sm = GameGlobal.sceneManager;
|
|
if (!sm._scenes.has(SCENE.TEAM_ROOM)) {
|
|
const TeamRoomScene = require('./TeamRoomScene');
|
|
sm.register(SCENE.TEAM_ROOM, TeamRoomScene);
|
|
}
|
|
sm.switchTo(SCENE.TEAM_ROOM, { teamId });
|
|
}, 100);
|
|
}
|
|
},
|
|
|
|
exit() {},
|
|
|
|
update(dt) {
|
|
this._tankAnim += dt;
|
|
},
|
|
|
|
render(ctx) {
|
|
// Background
|
|
ctx.fillStyle = COLORS.MENU_BG;
|
|
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
|
|
|
// Decorative top bar
|
|
const gradient = ctx.createLinearGradient(0, 0, SCREEN_WIDTH, 0);
|
|
gradient.addColorStop(0, '#0f3460');
|
|
gradient.addColorStop(0.5, '#e94560');
|
|
gradient.addColorStop(1, '#0f3460');
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, SCREEN_WIDTH, 4);
|
|
|
|
// Gold balance display at top
|
|
const gold = GameGlobal.currencyManager ? GameGlobal.currencyManager.getGold() : 0;
|
|
ctx.fillStyle = '#FFD700';
|
|
ctx.font = 'bold 14px Arial';
|
|
ctx.textAlign = 'right';
|
|
ctx.textBaseline = 'top';
|
|
ctx.fillText(`🪙 ${gold}`, SCREEN_WIDTH - 15, 12);
|
|
|
|
// Title
|
|
ctx.fillStyle = COLORS.MENU_TITLE;
|
|
ctx.font = 'bold 36px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(t('menu.title'), SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.15);
|
|
|
|
// Subtitle
|
|
ctx.fillStyle = '#AAAAAA';
|
|
ctx.font = '14px Arial';
|
|
ctx.fillText(t('menu.subtitle'), SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.22);
|
|
|
|
// Animated tank icon (simple oscillating triangle)
|
|
this._drawTankIcon(ctx, SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.30);
|
|
|
|
// Main game mode buttons (full width)
|
|
for (let i = 0; i < mainBtnRects.length; i++) {
|
|
const btn = mainBtnRects[i];
|
|
const isPressed = this._pressedIndex === i;
|
|
|
|
ctx.fillStyle = isPressed ? '#0f3460' : COLORS.MENU_BTN;
|
|
ctx.strokeStyle = COLORS.MENU_BTN_BORDER;
|
|
ctx.lineWidth = 2;
|
|
|
|
this._roundRect(ctx, btn.x, btn.y, btn.w, btn.h, 8);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = isPressed ? COLORS.MENU_TITLE : COLORS.MENU_BTN_TEXT;
|
|
ctx.font = 'bold 18px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(t(btn.labelKey), btn.x + btn.w / 2, btn.y + btn.h / 2);
|
|
}
|
|
|
|
// Utility buttons (2x2 grid, smaller font)
|
|
for (let i = 0; i < utilBtnRects.length; i++) {
|
|
const btn = utilBtnRects[i];
|
|
const globalIdx = mainBtnRects.length + i;
|
|
const isPressed = this._pressedIndex === globalIdx;
|
|
|
|
// Special rendering for daily gold button
|
|
const isDailyGold = btn.scene === 'DAILY_GOLD';
|
|
let label = t(btn.labelKey);
|
|
let btnColor = COLORS.MENU_BTN;
|
|
|
|
if (isDailyGold) {
|
|
const remaining = GameGlobal.adManager ? GameGlobal.adManager.getDailyGoldRemaining() : 0;
|
|
if (remaining > 0) {
|
|
label = `${t('dailyGold.btn')} ${remaining}/3`;
|
|
btnColor = '#2E7D32'; // green tint
|
|
} else {
|
|
label = t('dailyGold.exhausted') || 'Come back tomorrow';
|
|
btnColor = '#555555';
|
|
}
|
|
}
|
|
|
|
ctx.fillStyle = isPressed ? '#0f3460' : btnColor;
|
|
ctx.strokeStyle = COLORS.MENU_BTN_BORDER;
|
|
ctx.lineWidth = 1.5;
|
|
|
|
this._roundRect(ctx, btn.x, btn.y, btn.w, btn.h, 6);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = isPressed ? COLORS.MENU_TITLE : COLORS.MENU_BTN_TEXT;
|
|
ctx.font = 'bold 14px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(label, btn.x + btn.w / 2, btn.y + btn.h / 2);
|
|
}
|
|
|
|
// Footer
|
|
ctx.fillStyle = '#555555';
|
|
ctx.font = '11px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('v1.0.0', SCREEN_WIDTH / 2, SCREEN_HEIGHT - 20);
|
|
},
|
|
|
|
/**
|
|
* Draw a simple animated tank icon.
|
|
*/
|
|
_drawTankIcon(ctx, cx, cy) {
|
|
const bounce = Math.sin(this._tankAnim * 3) * 3;
|
|
const size = 20;
|
|
|
|
ctx.save();
|
|
ctx.translate(cx, cy + bounce);
|
|
|
|
// Tank body
|
|
ctx.fillStyle = COLORS.MENU_TITLE;
|
|
ctx.fillRect(-size, -size / 2, size * 2, size);
|
|
|
|
// Tank turret
|
|
ctx.fillRect(-3, -size / 2 - 14, 6, 14);
|
|
|
|
// Tank tracks
|
|
ctx.fillStyle = '#B8860B';
|
|
ctx.fillRect(-size - 4, -size / 2, 4, size);
|
|
ctx.fillRect(size, -size / 2, 4, size);
|
|
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* Draw a rounded rectangle path.
|
|
*/
|
|
_roundRect(ctx, x, y, w, h, r) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + r, y);
|
|
ctx.lineTo(x + w - r, y);
|
|
ctx.arcTo(x + w, y, x + w, y + r, r);
|
|
ctx.lineTo(x + w, y + h - r);
|
|
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
|
ctx.lineTo(x + r, y + h);
|
|
ctx.arcTo(x, y + h, x, y + h - r, r);
|
|
ctx.lineTo(x, y + r);
|
|
ctx.arcTo(x, y, x + r, y, r);
|
|
ctx.closePath();
|
|
},
|
|
|
|
handleTouch(eventType, e) {
|
|
if (eventType === 'touchstart') {
|
|
const touch = e.touches[0];
|
|
const tx = touch.clientX;
|
|
const ty = touch.clientY;
|
|
|
|
for (let i = 0; i < buttonRects.length; i++) {
|
|
const btn = buttonRects[i];
|
|
if (tx >= btn.x && tx <= btn.x + btn.w && ty >= btn.y && ty <= btn.y + btn.h) {
|
|
this._pressedIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
} else if (eventType === 'touchend') {
|
|
if (this._pressedIndex >= 0) {
|
|
const btn = buttonRects[this._pressedIndex];
|
|
this._pressedIndex = -1;
|
|
|
|
// Navigate to the target scene
|
|
const sm = GameGlobal.sceneManager;
|
|
if (btn.scene === SCENE.GAME) {
|
|
// Route through BuffSelectScene for PvE modes
|
|
if (!sm._scenes.has(SCENE.BUFF_SELECT)) {
|
|
const BuffSelectScene = require('./BuffSelectScene');
|
|
sm.register(SCENE.BUFF_SELECT, BuffSelectScene);
|
|
}
|
|
sm.switchTo(SCENE.BUFF_SELECT, { mode: btn.mode });
|
|
} else if (btn.scene === 'DAILY_GOLD') {
|
|
// Handle daily gold ad
|
|
const adm = GameGlobal.adManager;
|
|
if (adm && adm.getDailyGoldRemaining() > 0) {
|
|
adm.showDailyGoldAd((completed) => {
|
|
if (completed) {
|
|
try {
|
|
wx.showToast({ title: t('dailyGold.reward') || '+100 Gold!', icon: 'none', duration: 1500 });
|
|
} catch (e) {}
|
|
}
|
|
});
|
|
}
|
|
} else if (btn.scene === SCENE.SHOP) {
|
|
if (!sm._scenes.has(SCENE.SHOP)) {
|
|
const ShopScene = require('./ShopScene');
|
|
sm.register(SCENE.SHOP, ShopScene);
|
|
}
|
|
sm.switchTo(SCENE.SHOP);
|
|
} else if (btn.scene === SCENE.SKIN) {
|
|
if (!sm._scenes.has(SCENE.SKIN)) {
|
|
const SkinScene = require('./SkinScene');
|
|
sm.register(SCENE.SKIN, SkinScene);
|
|
}
|
|
sm.switchTo(SCENE.SKIN);
|
|
} else if (btn.scene === SCENE.SETTINGS) {
|
|
if (!sm._scenes.has(SCENE.SETTINGS)) {
|
|
const SettingsScene = require('./SettingsScene');
|
|
sm.register(SCENE.SETTINGS, SettingsScene);
|
|
}
|
|
sm.switchTo(SCENE.SETTINGS);
|
|
} else if (btn.scene === SCENE.RANKING) {
|
|
if (!sm._scenes.has(SCENE.RANKING)) {
|
|
const RankingScene = require('./RankingScene');
|
|
sm.register(SCENE.RANKING, RankingScene);
|
|
}
|
|
sm.switchTo(SCENE.RANKING);
|
|
} else if (btn.scene === SCENE.PVP_ROOM) {
|
|
if (!sm._scenes.has(SCENE.PVP_ROOM)) {
|
|
const RoomScene = require('./RoomScene');
|
|
sm.register(SCENE.PVP_ROOM, RoomScene);
|
|
}
|
|
sm.switchTo(SCENE.PVP_ROOM);
|
|
} else if (btn.scene === SCENE.TEAM_ROOM) {
|
|
if (!sm._scenes.has(SCENE.TEAM_ROOM)) {
|
|
const TeamRoomScene = require('./TeamRoomScene');
|
|
sm.register(SCENE.TEAM_ROOM, TeamRoomScene);
|
|
}
|
|
sm.switchTo(SCENE.TEAM_ROOM);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
module.exports = MenuScene;
|