d263c7bf48
- GameGlobal.js: keep upstream SERVER_URL with /ws suffix - en.js/zh.js: merge both settings.nickname and settings.profile keys - SettingsScene.js: keep both nickname row and profile button - server/index.js: merge express app + content security proxy with noServer WebSocket mode and path validation - Add .gitignore for node_modules and .codebuddy
332 lines
9.6 KiB
JavaScript
332 lines
9.6 KiB
JavaScript
/**
|
||
* SettingsScene.js
|
||
* Settings screen with sound, music, and vibration toggles.
|
||
*/
|
||
|
||
const {
|
||
SCREEN_WIDTH,
|
||
SCREEN_HEIGHT,
|
||
COLORS,
|
||
SCENE,
|
||
} = require('../base/GameGlobal');
|
||
const { t } = require('../i18n/I18n');
|
||
|
||
const SettingsScene = {
|
||
_settings: {
|
||
soundEnabled: true,
|
||
musicEnabled: true,
|
||
vibrationEnabled: true,
|
||
},
|
||
_buttons: {},
|
||
|
||
enter() {
|
||
// Load settings from storage
|
||
try {
|
||
const saved = wx.getStorageSync('game_settings');
|
||
if (saved) {
|
||
this._settings = { ...this._settings, ...JSON.parse(saved) };
|
||
}
|
||
} catch (e) {
|
||
console.warn('[Settings] Failed to load settings:', e);
|
||
}
|
||
this._buttons = {};
|
||
},
|
||
|
||
exit() {
|
||
// Save settings
|
||
try {
|
||
wx.setStorageSync('game_settings', JSON.stringify(this._settings));
|
||
} catch (e) {
|
||
console.warn('[Settings] Failed to save settings:', e);
|
||
}
|
||
},
|
||
|
||
update(dt) {},
|
||
|
||
render(ctx) {
|
||
// Background
|
||
ctx.fillStyle = COLORS.MENU_BG;
|
||
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||
|
||
const cx = SCREEN_WIDTH / 2;
|
||
|
||
// 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, titleY);
|
||
|
||
// 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;
|
||
|
||
// 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 (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);
|
||
}
|
||
}
|
||
|
||
// Profile entry button (below the last toggle row)
|
||
const profileY = firstCenterY + rows.length * step;
|
||
this._renderProfileButton(ctx, cx, profileY);
|
||
|
||
// Back button
|
||
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) {
|
||
const w = SCREEN_WIDTH * 0.7;
|
||
const h = 50;
|
||
const x = cx - w / 2;
|
||
const isOn = this._settings[toggle.key];
|
||
|
||
// Store button rect
|
||
this._buttons[toggle.key] = { 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 and label
|
||
ctx.fillStyle = COLORS.HUD_TEXT;
|
||
ctx.font = '16px Arial';
|
||
ctx.textAlign = 'left';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText(`${toggle.icon} ${toggle.label}`, x + 15, y);
|
||
|
||
// Toggle switch
|
||
const switchW = 50;
|
||
const switchH = 26;
|
||
const switchX = x + w - switchW - 15;
|
||
const switchY = y - switchH / 2;
|
||
|
||
// Switch track
|
||
ctx.fillStyle = isOn ? '#4CAF50' : '#555555';
|
||
ctx.beginPath();
|
||
ctx.arc(switchX + switchH / 2, y, switchH / 2, Math.PI / 2, Math.PI * 3 / 2);
|
||
ctx.arc(switchX + switchW - switchH / 2, y, switchH / 2, -Math.PI / 2, Math.PI / 2);
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
|
||
// Switch knob
|
||
ctx.fillStyle = '#FFFFFF';
|
||
ctx.beginPath();
|
||
const knobX = isOn ? switchX + switchW - switchH / 2 : switchX + switchH / 2;
|
||
ctx.arc(knobX, y, switchH / 2 - 3, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
},
|
||
|
||
_renderProfileButton(ctx, cx, y) {
|
||
const w = SCREEN_WIDTH * 0.7;
|
||
const h = 50;
|
||
const x = cx - w / 2;
|
||
|
||
this._buttons['profile'] = { 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 and label
|
||
ctx.fillStyle = COLORS.HUD_TEXT;
|
||
ctx.font = '16px Arial';
|
||
ctx.textAlign = 'left';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText(`👤 ${t('settings.profile')}`, x + 15, y);
|
||
|
||
// Arrow indicator
|
||
ctx.fillStyle = '#888888';
|
||
ctx.font = '14px Arial';
|
||
ctx.textAlign = 'right';
|
||
ctx.fillText('›', x + w - 15, y);
|
||
},
|
||
|
||
_renderBackButton(ctx, cx, y) {
|
||
const w = SCREEN_WIDTH * 0.4;
|
||
const h = 42;
|
||
const x = cx - w / 2;
|
||
|
||
this._buttons['back'] = { x, y: y - h / 2, w, h };
|
||
|
||
ctx.fillStyle = COLORS.MENU_BTN;
|
||
ctx.strokeStyle = COLORS.MENU_BTN_BORDER;
|
||
ctx.lineWidth = 2;
|
||
ctx.fillRect(x, y - h / 2, w, h);
|
||
ctx.strokeRect(x, y - h / 2, w, h);
|
||
|
||
ctx.fillStyle = COLORS.MENU_BTN_TEXT;
|
||
ctx.font = 'bold 16px Arial';
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText(t('common.back'), cx, y);
|
||
},
|
||
|
||
handleTouch(eventType, e) {
|
||
if (eventType !== 'touchstart') return;
|
||
|
||
const touch = e.touches[0];
|
||
const tx = touch.clientX;
|
||
const ty = touch.clientY;
|
||
|
||
for (const [key, rect] of Object.entries(this._buttons)) {
|
||
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 (key === 'profile') {
|
||
const sm = GameGlobal.sceneManager;
|
||
if (!sm._scenes.has(SCENE.PROFILE)) {
|
||
const ProfileScene = require('./ProfileScene');
|
||
sm.register(SCENE.PROFILE, ProfileScene);
|
||
}
|
||
sm.switchTo(SCENE.PROFILE);
|
||
} else if (this._settings.hasOwnProperty(key)) {
|
||
this._settings[key] = !this._settings[key];
|
||
// Notify audio system
|
||
GameGlobal.eventBus.emit('settings:changed', this._settings);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
|
||
// ============================================================
|
||
// 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;
|