/** * 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); } } // 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(); }, _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 (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;