Files
tankwar_proj/js/scenes/SettingsScene.js
2026-05-02 13:50:52 +08:00

293 lines
8.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;