chore: adjust player tank's size

This commit is contained in:
jakciehan
2026-05-02 13:50:52 +08:00
parent 0e321bcea6
commit 38294c040c
35 changed files with 5767 additions and 348 deletions
+1
View File
@@ -143,6 +143,7 @@ const GameScene = {
// Apply equipped skin colors to player tank
if (GameGlobal.skinManager) {
this._playerTank._skinColors = GameGlobal.skinManager.getCurrentSkinColors();
this._playerTank._skinId = GameGlobal.skinManager.getEquippedSkinId();
}
// Safety: ensure player spawn area is clear of blocking terrain
+337 -110
View File
@@ -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) => {
+138 -13
View File
@@ -49,32 +49,90 @@ const SettingsScene = {
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
const cx = SCREEN_WIDTH / 2;
let y = 60;
// Reset button map each frame so layout changes don't keep stale rects.
this._buttons = {};
// Title
const titleY = Math.max(48, SCREEN_HEIGHT * 0.08);
ctx.fillStyle = COLORS.MENU_TITLE;
ctx.font = 'bold 28px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(t('settings.title'), cx, y);
ctx.fillText(t('settings.title'), cx, titleY);
y += 70;
// Back button (reserved at bottom so we can layout rows above it).
const backH = 42;
const backMarginBottom = 28;
const backCenterY = SCREEN_HEIGHT - backMarginBottom - backH / 2;
// Toggle items
const toggles = [
{ key: 'soundEnabled', label: t('settings.sound'), icon: '🔊' },
{ key: 'musicEnabled', label: t('settings.music'), icon: '🎵' },
{ key: 'vibrationEnabled', label: t('settings.vibration'), icon: '📳' },
// Rows: nickname + 3 toggles. Distribute evenly between title and back btn.
const rows = [
{ type: 'nickname' },
{ type: 'toggle', key: 'soundEnabled', label: t('settings.sound'), icon: '🔊' },
{ type: 'toggle', key: 'musicEnabled', label: t('settings.music'), icon: '🎵' },
{ type: 'toggle', key: 'vibrationEnabled', label: t('settings.vibration'), icon: '📳' },
];
const rowH = 50;
const topPad = titleY + 36;
const bottomPad = backCenterY - backH / 2 - 20;
const availH = Math.max(rowH * rows.length, bottomPad - topPad);
const step = Math.max(rowH + 8, availH / rows.length);
const firstCenterY = topPad + step / 2;
for (const toggle of toggles) {
this._renderToggle(ctx, cx, y, toggle);
y += 70;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const cy = firstCenterY + i * step;
if (row.type === 'nickname') {
this._renderNicknameRow(ctx, cx, cy);
} else {
this._renderToggle(ctx, cx, cy, row);
}
}
// Back button
y = SCREEN_HEIGHT - 80;
this._renderBackButton(ctx, cx, y);
this._renderBackButton(ctx, cx, backCenterY);
},
_renderNicknameRow(ctx, cx, y) {
const w = SCREEN_WIDTH * 0.7;
const h = 50;
const x = cx - w / 2;
this._buttons['nickname'] = { x, y: y - h / 2, w, h };
// Background
ctx.fillStyle = '#1e1e3a';
ctx.fillRect(x, y - h / 2, w, h);
ctx.strokeStyle = '#333366';
ctx.lineWidth = 1;
ctx.strokeRect(x, y - h / 2, w, h);
// Icon + label (left)
ctx.fillStyle = COLORS.HUD_TEXT;
ctx.font = '16px Arial';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const leftLabel = `👤 ${t('settings.nickname') || '显示名字'}`;
ctx.fillText(leftLabel, x + 15, y);
// Current value + chevron (right)
const profile = GameGlobal.playerProfile;
let shown = '';
if (profile) {
if (profile.granted && profile.nickname) {
shown = profile.truncate ? profile.truncate(profile.nickname, 5) : profile.nickname;
} else if (typeof profile.getDisplayName === 'function') {
const pid = (GameGlobal.networkManager && GameGlobal.networkManager.playerId) || '';
shown = profile.getDisplayName(pid);
}
}
if (!shown) shown = 'Tanker';
ctx.fillStyle = profile && profile.granted ? '#FFD700' : '#8899AA';
ctx.font = '13px Arial';
ctx.textAlign = 'right';
ctx.fillText(`${shown} `, x + w - 15, y);
},
_renderToggle(ctx, cx, y, toggle) {
@@ -153,6 +211,10 @@ const SettingsScene = {
if (tx >= rect.x && tx <= rect.x + rect.w && ty >= rect.y && ty <= rect.y + rect.h) {
if (key === 'back') {
GameGlobal.sceneManager.switchTo(SCENE.MENU);
} else if (key === 'nickname') {
// IMPORTANT: wx.getUserProfile must be called synchronously from a
// user tap handler; invoking it here is fine (touchstart is a tap).
this._requestNicknameAuth();
} else if (this._settings.hasOwnProperty(key)) {
this._settings[key] = !this._settings[key];
// Notify audio system
@@ -162,6 +224,69 @@ const SettingsScene = {
}
}
},
// ============================================================
// Nickname acquisition (moved from MenuScene)
// ============================================================
_requestNicknameAuth() {
const profile = GameGlobal.playerProfile;
if (!profile) return;
const onDone = (ok) => {
if (ok) {
try {
wx.showToast({
title: `欢迎 ${profile.nickname}`,
icon: 'none',
duration: 1500,
});
} catch (e) { /* ignore */ }
}
};
if (typeof profile.requestUserProfile === 'function') {
profile.requestUserProfile().then((ok) => {
if (ok) {
onDone(true);
} else {
this._promptManualNickname(onDone);
}
}).catch(() => this._promptManualNickname(onDone));
} else {
this._promptManualNickname(onDone);
}
},
_promptManualNickname(cb) {
try {
if (typeof wx === 'undefined' || typeof wx.showModal !== 'function') {
cb && cb(false);
return;
}
wx.showModal({
title: '设置昵称',
content: '输入在对战中显示的名字(最长16字)',
editable: true,
placeholderText: '例如:坏蹄子',
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
const profile = GameGlobal.playerProfile;
const ok = profile && typeof profile.setManualNickname === 'function'
&& profile.setManualNickname(res.content || '');
cb && cb(!!ok);
} else {
cb && cb(false);
}
},
fail: () => cb && cb(false),
});
} catch (e) {
console.warn('[Settings] showModal failed:', e && e.message);
cb && cb(false);
}
},
};
module.exports = SettingsScene;
+1481 -163
View File
File diff suppressed because it is too large Load Diff
+97 -10
View File
@@ -240,6 +240,11 @@ const TeamGameScene = {
tank.color = tankColor;
// Unlimited lives for 3v3
tank.lives = 999;
// Apply equipped skin (only for the LOCAL player — other players keep team color)
if (GameGlobal.skinManager && isLocal) {
tank._skinColors = GameGlobal.skinManager.getCurrentSkinColors();
tank._skinId = GameGlobal.skinManager.getEquippedSkinId();
}
}
tank.activateShield(3000);
@@ -256,6 +261,7 @@ const TeamGameScene = {
const playerData = {
playerId: member.playerId,
nickname: member.nickname || '',
tank,
isBot,
team,
@@ -441,6 +447,29 @@ const TeamGameScene = {
}
}));
// Receive live team roster updates — keeps every tank's overhead label in
// sync with the real WeChat nickname, which may be granted AFTER the match
// has already started (via MenuScene's UserInfoButton).
unsubs.push(nm.on(NET_MSG.TEAM_STATE, (data) => {
if (!data) return;
const rosterA = Array.isArray(data.teamA) ? data.teamA : [];
const rosterB = Array.isArray(data.teamB) ? data.teamB : [];
const byId = Object.create(null);
for (const m of rosterA) if (m && m.playerId) byId[m.playerId] = m.nickname || '';
for (const m of rosterB) if (m && m.playerId) byId[m.playerId] = m.nickname || '';
let changed = false;
for (const p of this._players) {
const nn = byId[p.playerId];
if (nn && p.nickname !== nn) {
p.nickname = nn;
changed = true;
}
}
if (changed) {
console.log('[TeamGameScene] Roster nicknames refreshed.');
}
}));
this._unsubscribers = unsubs;
},
@@ -1126,6 +1155,7 @@ const TeamGameScene = {
stats: this._stats,
players: this._players.map(p => ({
playerId: p.playerId,
nickname: p.nickname || '',
team: p.team,
isBot: p.isBot,
isLocal: p.isLocal,
@@ -1154,16 +1184,41 @@ const TeamGameScene = {
if (player.tank.alive && !player.isRespawning) {
player.tank.render(ctx);
// Draw team indicator above tank
if (!player.isLocal) {
const tx = player.tank.x;
const ty = player.tank.y - player.tank.halfSize - 8;
ctx.fillStyle = player.team === this._myTeam ? TEAM_A_COLOR : TEAM_B_COLOR;
ctx.font = 'bold 8px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const label = player.isBot ? '🤖' : (player.team === this._myTeam ? '▲' : '▼');
ctx.fillText(label, tx, ty);
// Name & team indicator above the tank
const tx = player.tank.x;
const labelY = player.tank.y - player.tank.halfSize - 4;
const nameY = labelY - 10;
// Per-tank team color:
// - local player → gold
// - ally (not me) → blue
// - enemy → red
let labelColor;
if (player.isLocal) labelColor = LOCAL_PLAYER_COLOR;
else if (player.team === this._myTeam) labelColor = TEAM_A_COLOR;
else labelColor = TEAM_B_COLOR;
ctx.fillStyle = labelColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Arrow / bot tag
ctx.font = 'bold 8px Arial';
let marker;
if (player.isLocal) marker = '★';
else if (player.isBot) marker = '🤖';
else marker = (player.team === this._myTeam) ? '▲' : '▼';
ctx.fillText(marker, tx, labelY);
// Nickname (truncated to 4 Chinese-equivalent chars)
const name = this._getTankLabel(player);
if (name) {
ctx.font = 'bold 9px Arial';
// Outline for readability on busy backgrounds
ctx.lineWidth = 3;
ctx.strokeStyle = 'rgba(0,0,0,0.7)';
ctx.strokeText(name, tx, nameY);
ctx.fillText(name, tx, nameY);
}
}
}
@@ -1220,6 +1275,38 @@ const TeamGameScene = {
return { kills, deaths };
},
/**
* Compute a short label ( 4 Chinese-equivalent chars) to draw above a tank.
* Uses real WeChat nickname if available, otherwise a stable fallback.
* @private
*/
_getTankLabel(player) {
if (!player) return '';
const profile = GameGlobal.playerProfile;
let raw = '';
if (player.isLocal) {
// For local player prefer the freshest profile nickname if granted.
if (profile && profile.nickname) raw = profile.nickname;
else raw = player.nickname || '';
} else {
raw = player.nickname || '';
}
if (!raw) {
if (player.isBot) {
raw = ''; // bot — we already draw the 🤖 marker, skip name
} else if (profile && typeof profile.getDisplayName === 'function') {
raw = profile.getDisplayName(player.playerId);
} else {
raw = player.playerId || '';
}
}
if (!raw) return '';
if (profile && typeof profile.truncate === 'function') {
return profile.truncate(raw, 4);
}
return raw.length > 8 ? raw.substring(0, 8) + '..' : raw;
},
_renderHUD(ctx) {
const hudY = 4;
+29 -3
View File
@@ -358,7 +358,7 @@ const TeamResultScene = {
ctx.fillStyle = isLocal ? '#FFD700' : '#CCCCCC';
ctx.font = isLocal ? 'bold 10px Arial' : '10px Arial';
ctx.textAlign = 'center';
const name = player.isBot ? t('teamResult.bot') : (player.playerId.length > 10 ? player.playerId.substring(0, 10) + '..' : player.playerId);
const name = player.isBot ? t('teamResult.bot') : this._getDisplayName(player);
ctx.fillText(name + (isLocal ? ' ★' : ''), cols.name, y + rowH / 2);
// Stats
@@ -406,7 +406,7 @@ const TeamResultScene = {
ctx.fillStyle = isLocal ? '#FFD700' : '#CCCCCC';
ctx.font = isLocal ? 'bold 10px Arial' : '10px Arial';
ctx.textAlign = 'center';
const name = player.isBot ? t('teamResult.bot') : (player.playerId.length > 10 ? player.playerId.substring(0, 10) + '..' : player.playerId);
const name = player.isBot ? t('teamResult.bot') : this._getDisplayName(player);
ctx.fillText(name + (isLocal ? ' ★' : ''), cols.name, y + rowH / 2);
// Stats
@@ -451,7 +451,7 @@ const TeamResultScene = {
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 11px Arial';
ctx.textAlign = 'center';
const mvpName = mvp.isBot ? t('teamResult.bot').replace('🤖 ', '') : mvp.playerId;
const mvpName = mvp.isBot ? t('teamResult.bot').replace('🤖 ', '') : this._getDisplayName(mvp);
ctx.fillText(t('teamResult.mvp', { name: mvpName, kills: maxKills }), CENTER_X, y);
}
@@ -583,6 +583,32 @@ const TeamResultScene = {
return;
}
},
/**
* Compute a display name for the results table ( 4 CJK chars).
* @private
*/
_getDisplayName(player) {
if (!player) return '';
const profile = GameGlobal.playerProfile;
let raw = '';
if (player.isLocal && profile && profile.nickname) {
raw = profile.nickname;
} else {
raw = player.nickname || '';
}
if (!raw) {
if (profile && typeof profile.getDisplayName === 'function') {
raw = profile.getDisplayName(player.playerId);
} else {
raw = player.playerId || '';
}
}
if (profile && typeof profile.truncate === 'function') {
return profile.truncate(raw, 4);
}
return raw.length > 10 ? raw.substring(0, 10) + '..' : raw;
},
};
module.exports = TeamResultScene;
+25 -2
View File
@@ -419,7 +419,7 @@ const TeamRoomScene = {
// Player name (truncated)
ctx.fillStyle = '#FFFFFF';
ctx.font = '10px Arial';
const name = member.playerId.length > 8 ? member.playerId.substring(0, 8) + '..' : member.playerId;
const name = this._getDisplayName(member);
ctx.fillText(name, avatarCX, rect.y + rect.h * 0.7);
// Ready state
@@ -488,7 +488,7 @@ const TeamRoomScene = {
ctx.font = '10px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const name = member.playerId.length > 8 ? member.playerId.substring(0, 8) + '..' : member.playerId;
const name = this._getDisplayName(member);
ctx.fillText(name, rect.x + rect.w / 2, rect.y + rect.h / 2);
}
}
@@ -575,6 +575,29 @@ const TeamRoomScene = {
return tx >= rect.x && tx <= rect.x + rect.w && ty >= rect.y && ty <= rect.y + rect.h;
},
/**
* Compute a display name for a team member entry.
* Uses real WeChat nickname when available, otherwise a stable fallback.
* Truncated to 4 Chinese-equivalent chars to fit the slot UI.
* @private
*/
_getDisplayName(member) {
if (!member) return '';
const profile = GameGlobal.playerProfile;
let raw = member.nickname || '';
if (!raw) {
if (profile && typeof profile.getDisplayName === 'function') {
raw = profile.getDisplayName(member.playerId);
} else {
raw = member.playerId || '';
}
}
if (profile && typeof profile.truncate === 'function') {
return profile.truncate(raw, 4);
}
return raw.length > 8 ? raw.substring(0, 8) + '..' : raw;
},
handleTouch(eventType, e) {
if (eventType !== 'touchstart') return;