chore: adjust player tank's size
This commit is contained in:
+337
-110
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* MenuScene.js
|
||||
* Main menu scene - displays game title and mode selection buttons.
|
||||
* Main menu scene — military-tech themed UI with game title and mode selection.
|
||||
* Rendered entirely with Canvas API (no DOM).
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,26 @@ const {
|
||||
} = require('../base/GameGlobal');
|
||||
const { t } = require('../i18n/I18n');
|
||||
|
||||
// ============================================================
|
||||
// Style
|
||||
// ============================================================
|
||||
const MC = {
|
||||
BG_TOP: '#0b0e17',
|
||||
BG_BOT: '#141b2d',
|
||||
ACCENT: '#e94560',
|
||||
GOLD: '#FFD700',
|
||||
GOLD_DIM: '#B8860B',
|
||||
BTN_BG: '#16213e',
|
||||
BTN_BORDER: '#1e3054',
|
||||
BTN_HOVER: '#0f3460',
|
||||
BTN_TEXT: '#E8E8E8',
|
||||
TITLE: '#FFD700',
|
||||
SUBTITLE: '#8899AA',
|
||||
FOOTER: '#445566',
|
||||
TANK_BODY: '#FFD700',
|
||||
TANK_TRACK: '#B8860B',
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Button Layout
|
||||
// ============================================================
|
||||
@@ -22,9 +42,7 @@ 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)
|
||||
@@ -35,9 +53,9 @@ const MAIN_BUTTONS = [
|
||||
{ labelKey: 'menu.team3v3', mode: GAME_MODE.TEAM_3V3, scene: SCENE.TEAM_ROOM },
|
||||
];
|
||||
|
||||
// Utility buttons: shop, daily gold, skin, ranking, settings (grid)
|
||||
// Utility buttons: daily gold, skin, ranking, settings (grid)
|
||||
// NOTE: Shop button is temporarily disabled
|
||||
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 },
|
||||
@@ -53,32 +71,20 @@ const mainBtnRects = MAIN_BUTTONS.map((btn, i) => ({
|
||||
...btn,
|
||||
}));
|
||||
|
||||
// Pre-calculate button rects for utility buttons (row1: 3 cols, row2: 2 cols centered)
|
||||
// Pre-calculate button rects for utility buttons (2 rows x 2 cols)
|
||||
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,
|
||||
};
|
||||
}
|
||||
const row = Math.floor(i / 2);
|
||||
const col = i % 2;
|
||||
return {
|
||||
x: BTN_X + col * (HALF_BTN_WIDTH + BTN_GAP),
|
||||
y: utilStartY + row * (BTN_HEIGHT + BTN_GAP),
|
||||
w: HALF_BTN_WIDTH,
|
||||
h: BTN_HEIGHT,
|
||||
...btn,
|
||||
};
|
||||
});
|
||||
|
||||
// Combined list for unified iteration
|
||||
const buttonRects = [...mainBtnRects, ...utilBtnRects];
|
||||
|
||||
// ============================================================
|
||||
@@ -86,18 +92,20 @@ const buttonRects = [...mainBtnRects, ...utilBtnRects];
|
||||
// ============================================================
|
||||
const MenuScene = {
|
||||
_pressedIndex: -1,
|
||||
_tankAnim: 0, // simple animation timer
|
||||
_tankAnim: 0,
|
||||
|
||||
enter() {
|
||||
this._pressedIndex = -1;
|
||||
this._tankAnim = 0;
|
||||
|
||||
// Auto-navigate to team room if there's a pending invite teamId
|
||||
// Kick off nickname acquisition as early as possible so that later
|
||||
// network messages (CREATE_TEAM, SOLO_MATCH, ...) can carry it.
|
||||
this._initPlayerProfile();
|
||||
|
||||
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;
|
||||
@@ -110,140 +118,338 @@ const MenuScene = {
|
||||
}
|
||||
},
|
||||
|
||||
exit() {},
|
||||
exit() {
|
||||
this._pressedIndex = -1;
|
||||
},
|
||||
|
||||
update(dt) {
|
||||
this._tankAnim += dt;
|
||||
},
|
||||
|
||||
render(ctx) {
|
||||
// Background
|
||||
ctx.fillStyle = COLORS.MENU_BG;
|
||||
// ---- Background ----
|
||||
const bgGrad = ctx.createLinearGradient(0, 0, 0, SCREEN_HEIGHT);
|
||||
bgGrad.addColorStop(0, MC.BG_TOP);
|
||||
bgGrad.addColorStop(1, MC.BG_BOT);
|
||||
ctx.fillStyle = bgGrad;
|
||||
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);
|
||||
// Scan-lines
|
||||
ctx.globalAlpha = 0.025;
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
for (let sy = 0; sy < SCREEN_HEIGHT; sy += 4) {
|
||||
ctx.fillRect(0, sy, SCREEN_WIDTH, 1);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Gold balance display at top
|
||||
// Top accent bar
|
||||
const accentGrad = ctx.createLinearGradient(0, 0, SCREEN_WIDTH, 0);
|
||||
accentGrad.addColorStop(0, 'transparent');
|
||||
accentGrad.addColorStop(0.3, MC.ACCENT);
|
||||
accentGrad.addColorStop(0.7, MC.ACCENT);
|
||||
accentGrad.addColorStop(1, 'transparent');
|
||||
ctx.fillStyle = accentGrad;
|
||||
ctx.fillRect(0, 0, SCREEN_WIDTH, 3);
|
||||
|
||||
// ---- Gold Balance (top-right pill) ----
|
||||
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);
|
||||
const goldText = `🪙 ${gold}`;
|
||||
ctx.font = 'bold 12px Arial';
|
||||
const gtw = ctx.measureText(goldText).width;
|
||||
const pillW = gtw + 16;
|
||||
const pillH = 22;
|
||||
const pillX = SCREEN_WIDTH - pillW - 12;
|
||||
const pillY = 10;
|
||||
|
||||
// Title
|
||||
ctx.fillStyle = COLORS.MENU_TITLE;
|
||||
ctx.font = 'bold 36px Arial';
|
||||
ctx.fillStyle = 'rgba(255, 215, 0, 0.08)';
|
||||
ctx.strokeStyle = 'rgba(255, 215, 0, 0.3)';
|
||||
ctx.lineWidth = 1;
|
||||
this._roundRect(ctx, pillX, pillY, pillW, pillH, pillH / 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = MC.GOLD;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(goldText, pillX + pillW / 2, pillY + pillH / 2);
|
||||
|
||||
// ---- Title with glow ----
|
||||
ctx.save();
|
||||
ctx.shadowColor = MC.GOLD;
|
||||
ctx.shadowBlur = 16;
|
||||
ctx.fillStyle = MC.TITLE;
|
||||
ctx.font = 'bold 34px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(t('menu.title'), SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.15);
|
||||
ctx.restore();
|
||||
|
||||
// Subtitle
|
||||
ctx.fillStyle = '#AAAAAA';
|
||||
ctx.font = '14px Arial';
|
||||
// ---- Subtitle ----
|
||||
ctx.fillStyle = MC.SUBTITLE;
|
||||
ctx.font = '13px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(t('menu.subtitle'), SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.22);
|
||||
|
||||
// Animated tank icon (simple oscillating triangle)
|
||||
// ---- Animated Tank Icon ----
|
||||
this._drawTankIcon(ctx, SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.30);
|
||||
|
||||
// Main game mode buttons (full width)
|
||||
// ---- Main Buttons ----
|
||||
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);
|
||||
this._drawMenuButton(ctx, btn, t(btn.labelKey), isPressed, 'bold 16px Arial', 8);
|
||||
}
|
||||
|
||||
// Utility buttons (2x2 grid, smaller font)
|
||||
// ---- Utility Buttons ----
|
||||
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;
|
||||
let customBg = null;
|
||||
|
||||
if (isDailyGold) {
|
||||
const remaining = GameGlobal.adManager ? GameGlobal.adManager.getDailyGoldRemaining() : 0;
|
||||
if (remaining > 0) {
|
||||
label = `${t('dailyGold.btn')} ${remaining}/3`;
|
||||
btnColor = '#2E7D32'; // green tint
|
||||
customBg = '#1a3a2a';
|
||||
} else {
|
||||
label = t('dailyGold.exhausted') || 'Come back tomorrow';
|
||||
btnColor = '#555555';
|
||||
customBg = '#2a2a2a';
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
this._drawMenuButton(ctx, btn, label, isPressed, 'bold 13px Arial', 6, customBg);
|
||||
}
|
||||
|
||||
// Footer
|
||||
ctx.fillStyle = '#555555';
|
||||
ctx.font = '11px Arial';
|
||||
// ---- Footer ----
|
||||
ctx.fillStyle = MC.FOOTER;
|
||||
ctx.font = '10px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('v1.0.0', SCREEN_WIDTH / 2, SCREEN_HEIGHT - 20);
|
||||
ctx.fillText('v1.0.0', SCREEN_WIDTH / 2, SCREEN_HEIGHT - 18);
|
||||
},
|
||||
|
||||
/**
|
||||
* Draw a simple animated tank icon.
|
||||
*/
|
||||
// ---- Menu Button ----
|
||||
_drawMenuButton(ctx, btn, label, isPressed, font, radius, customBg) {
|
||||
const r = radius || 8;
|
||||
|
||||
// Shadow
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
||||
this._roundRect(ctx, btn.x + 1, btn.y + 2, btn.w, btn.h, r);
|
||||
ctx.fill();
|
||||
|
||||
// Body
|
||||
ctx.fillStyle = isPressed ? MC.BTN_HOVER : (customBg || MC.BTN_BG);
|
||||
ctx.strokeStyle = MC.BTN_BORDER;
|
||||
ctx.lineWidth = 1.5;
|
||||
this._roundRect(ctx, btn.x, btn.y, btn.w, btn.h, r);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
// Label
|
||||
ctx.fillStyle = isPressed ? MC.TITLE : MC.BTN_TEXT;
|
||||
ctx.font = font || 'bold 16px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(label, btn.x + btn.w / 2, btn.y + btn.h / 2);
|
||||
},
|
||||
|
||||
// ---- Tank Icon ----
|
||||
_drawTankIcon(ctx, cx, cy) {
|
||||
const bounce = Math.sin(this._tankAnim * 3) * 3;
|
||||
const size = 20;
|
||||
const bounce = Math.sin(this._tankAnim * 3) * 2;
|
||||
const pulse = 0.5 + 0.5 * Math.sin(this._tankAnim * 2);
|
||||
const s = 15; // body half-size (square tank: 2s × 2s)
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(cx, cy + bounce);
|
||||
|
||||
// Tank body
|
||||
ctx.fillStyle = COLORS.MENU_TITLE;
|
||||
ctx.fillRect(-size, -size / 2, size * 2, size);
|
||||
// ── 1. OUTER GLOW HALO (breathing golden aura) ──
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.15 + pulse * 0.12;
|
||||
ctx.fillStyle = MC.GOLD;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, s * 1.55, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// Tank turret
|
||||
ctx.fillRect(-3, -size / 2 - 14, 6, 14);
|
||||
// ── 2. GROUND SHADOW (soft ellipse underneath) ──
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.35;
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, s + 6, s * 1.1, 3, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// Tank tracks
|
||||
ctx.fillStyle = '#B8860B';
|
||||
ctx.fillRect(-size - 4, -size / 2, 4, size);
|
||||
ctx.fillRect(size, -size / 2, 4, size);
|
||||
// ── 3. TRACKS (left & right, with segment pattern) ──
|
||||
const trackW = 5;
|
||||
const trackX = s;
|
||||
// Left track
|
||||
ctx.fillStyle = '#4A3508';
|
||||
this._roundRect(ctx, -trackX - trackW, -s, trackW, s * 2, 1.5);
|
||||
ctx.fill();
|
||||
// Right track
|
||||
this._roundRect(ctx, trackX, -s, trackW, s * 2, 1.5);
|
||||
ctx.fill();
|
||||
// Track top highlight
|
||||
ctx.fillStyle = 'rgba(255, 220, 120, 0.35)';
|
||||
ctx.fillRect(-trackX - trackW + 0.8, -s + 0.8, trackW - 1.6, 1);
|
||||
ctx.fillRect(trackX + 0.8, -s + 0.8, trackW - 1.6, 1);
|
||||
// Track segment lines (metallic plate pattern)
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.55)';
|
||||
ctx.lineWidth = 0.8;
|
||||
for (let ty = -s + 4; ty < s - 1; ty += 4) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-trackX - trackW, ty);
|
||||
ctx.lineTo(-trackX, ty);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(trackX, ty);
|
||||
ctx.lineTo(trackX + trackW, ty);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// ── 4. BODY (square, with vertical metallic gradient) ──
|
||||
ctx.save();
|
||||
ctx.shadowColor = MC.GOLD;
|
||||
ctx.shadowBlur = 12;
|
||||
const bodyGrad = ctx.createLinearGradient(0, -s, 0, s);
|
||||
bodyGrad.addColorStop(0, '#FFF3A8'); // top highlight
|
||||
bodyGrad.addColorStop(0.3, MC.GOLD); // main gold
|
||||
bodyGrad.addColorStop(0.75, '#C89A1C');
|
||||
bodyGrad.addColorStop(1, '#7A5A0A'); // bottom shadow
|
||||
ctx.fillStyle = bodyGrad;
|
||||
this._roundRect(ctx, -s, -s, s * 2, s * 2, 3);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// ── 5. BODY EDGE OUTLINE ──
|
||||
ctx.strokeStyle = 'rgba(255, 235, 150, 0.7)';
|
||||
ctx.lineWidth = 1;
|
||||
this._roundRect(ctx, -s, -s, s * 2, s * 2, 3);
|
||||
ctx.stroke();
|
||||
|
||||
// ── 6. TOP HIGHLIGHT STRIP (bright metallic sheen) ──
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.35)';
|
||||
this._roundRect(ctx, -s + 2, -s + 2, s * 2 - 4, 3, 1.5);
|
||||
ctx.fill();
|
||||
|
||||
// ── 7. ARMOR PLATE DETAIL (X-cross rivets in 4 corners + center crosshair) ──
|
||||
const rivetOffset = s * 0.6;
|
||||
ctx.fillStyle = '#6B4A08';
|
||||
for (const [rx, ry] of [[-rivetOffset, -rivetOffset], [rivetOffset, -rivetOffset],
|
||||
[-rivetOffset, rivetOffset], [rivetOffset, rivetOffset]]) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(rx, ry, 1.4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
// Tiny rivet highlight
|
||||
ctx.fillStyle = 'rgba(255, 240, 180, 0.8)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(rx - 0.3, ry - 0.3, 0.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#6B4A08';
|
||||
}
|
||||
|
||||
// ── 8. TURRET (diamond-shaped center cap with gradient) ──
|
||||
const turretR = s * 0.42;
|
||||
ctx.save();
|
||||
ctx.shadowColor = MC.GOLD;
|
||||
ctx.shadowBlur = 6;
|
||||
const turretGrad = ctx.createRadialGradient(-turretR * 0.3, -turretR * 0.3, 0, 0, 0, turretR);
|
||||
turretGrad.addColorStop(0, '#FFF3A8');
|
||||
turretGrad.addColorStop(0.5, '#E0B020');
|
||||
turretGrad.addColorStop(1, '#8B6914');
|
||||
ctx.fillStyle = turretGrad;
|
||||
// Diamond (rotated square) for "military hatch" feel
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -turretR);
|
||||
ctx.lineTo(turretR, 0);
|
||||
ctx.lineTo(0, turretR);
|
||||
ctx.lineTo(-turretR, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
// Turret edge
|
||||
ctx.strokeStyle = 'rgba(255, 235, 150, 0.8)';
|
||||
ctx.lineWidth = 0.8;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -turretR);
|
||||
ctx.lineTo(turretR, 0);
|
||||
ctx.lineTo(0, turretR);
|
||||
ctx.lineTo(-turretR, 0);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
|
||||
// Turret internal cross
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.35)';
|
||||
ctx.lineWidth = 0.6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -turretR); ctx.lineTo(0, turretR);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-turretR, 0); ctx.lineTo(turretR, 0);
|
||||
ctx.stroke();
|
||||
|
||||
// Turret center hatch
|
||||
ctx.fillStyle = '#FFF3A8';
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// ── 9. BARREL (thick, with metallic gradient) ──
|
||||
const barrelW = 6;
|
||||
const barrelH = 16;
|
||||
const barrelY = -s - barrelH;
|
||||
ctx.save();
|
||||
const barrelGrad = ctx.createLinearGradient(-barrelW / 2, 0, barrelW / 2, 0);
|
||||
barrelGrad.addColorStop(0, '#6B4A08');
|
||||
barrelGrad.addColorStop(0.3, '#B8860B');
|
||||
barrelGrad.addColorStop(0.5, '#FFF3A8');
|
||||
barrelGrad.addColorStop(0.7, '#B8860B');
|
||||
barrelGrad.addColorStop(1, '#6B4A08');
|
||||
ctx.fillStyle = barrelGrad;
|
||||
this._roundRect(ctx, -barrelW / 2, barrelY, barrelW, barrelH, 1.5);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
// Barrel outline
|
||||
ctx.strokeStyle = 'rgba(255, 235, 150, 0.55)';
|
||||
ctx.lineWidth = 0.6;
|
||||
this._roundRect(ctx, -barrelW / 2, barrelY, barrelW, barrelH, 1.5);
|
||||
ctx.stroke();
|
||||
|
||||
// ── 10. MUZZLE TIP (flared end with glow) ──
|
||||
ctx.save();
|
||||
ctx.shadowColor = '#FFF3A8';
|
||||
ctx.shadowBlur = 5 + pulse * 3;
|
||||
ctx.fillStyle = '#2A1D05';
|
||||
this._roundRect(ctx, -barrelW / 2 - 1, barrelY - 2, barrelW + 2, 3, 1);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
// Muzzle inner bright dot
|
||||
ctx.fillStyle = `rgba(255, 240, 150, ${0.6 + pulse * 0.4})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, barrelY - 0.5, 1, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// ── 11. HEADLIGHTS (front of tank — 2 small glowing dots) ──
|
||||
ctx.fillStyle = `rgba(255, 255, 200, ${0.7 + pulse * 0.3})`;
|
||||
ctx.shadowColor = '#FFF3A8';
|
||||
ctx.shadowBlur = 4;
|
||||
ctx.beginPath();
|
||||
ctx.arc(-s * 0.55, -s + 2.5, 1.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(s * 0.55, -s + 2.5, 1.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
/**
|
||||
* Draw a rounded rectangle path.
|
||||
*/
|
||||
// ---- Utility ----
|
||||
_roundRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
@@ -258,6 +464,30 @@ const MenuScene = {
|
||||
ctx.closePath();
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// Player Profile (nickname acquisition)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Kick off profile acquisition on menu enter. Since WeChat 2022-10 there
|
||||
* is NO silent way to get the real nickname — we draw a canvas button and
|
||||
* call `wx.getUserProfile` directly from its touchend handler.
|
||||
* @private
|
||||
*/
|
||||
_initPlayerProfile() {
|
||||
const profile = GameGlobal.playerProfile;
|
||||
if (!profile) return;
|
||||
|
||||
// Best-effort placeholder fetch (used only to pre-fill _nickname with
|
||||
// "微信用户" on older devices; does not mark granted).
|
||||
if (typeof profile.fetchSilent === 'function') {
|
||||
profile.fetchSilent().catch(() => { /* ignore */ });
|
||||
}
|
||||
},
|
||||
|
||||
// ============================================================
|
||||
// Touch
|
||||
// ============================================================
|
||||
handleTouch(eventType, e) {
|
||||
if (eventType === 'touchstart') {
|
||||
const touch = e.touches[0];
|
||||
@@ -276,17 +506,14 @@ const MenuScene = {
|
||||
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) => {
|
||||
|
||||
Reference in New Issue
Block a user