// Player settings (BGM/SFX/voice volume, mute, damage numbers, render quality): a tiny observable store // persisted in localStorage (`sp.pref.settings`), applied to the audio manager on every change, plus // the settings modal — which also holds the language switch (ui/lang.js; kept apart in `sp.pref.lang`). import { useState } from '../../vendor/hooks.module.js'; import { html, Modal, Button, Icon, MicroLabel } from './components.js'; import { createStore, useStore, loadPref, savePref } from '../store.js'; import { sanitizeSettings } from './gameLogic.js'; import { audio } from '../audio.js'; import { openGuide } from './guide.js'; import { detectFeatures } from './device.js'; import { LangToggle } from './lang.js'; import { t, tc, N_ } from '../../../shared/i18n.js'; /** Settings store: { bgm, sfx, voice, muted, damageNumbers, quality }. */ export const settingsStore = createStore(sanitizeSettings(loadPref('settings', null))); settingsStore.subscribe((s) => { savePref('settings', sanitizeSettings(s)); audio.setVolumes(s); }); audio.setVolumes(settingsStore.get()); /** @param {Partial>} patch */ export function updateSettings(patch) { settingsStore.set(sanitizeSettings({ ...settingsStore.get(), ...patch })); } /** Preact hook: current settings. */ export const useSettings = () => useStore((s) => s, Object.is, settingsStore); function Slider({ label, micro, value, onInput, icon }) { const pct = Math.round(value * 100); return html``; } function Toggle({ label, micro, value, onChange }) { return html`
${label}<${MicroLabel}>${micro}
`; } const QUALITY = [['high', N_('高')], ['medium', N_('中')], ['low', N_('低')]]; /** Desktop shortcuts of the hint line: key → action (msgids). */ const SHORTCUTS = [['R', N_('刷新')], ['F', N_('冻结')], ['D', N_('升级')], ['Q', N_('撤退选中干员')], ['X', N_('出售选中干员')], ['Space', N_('准备就绪')], ['Esc', N_('关闭弹窗')]]; /** * Settings modal. * @param {{ open: boolean, onClose: Function }} props */ export function SettingsModal({ open, onClose }) { const s = useSettings(); const [tested, setTested] = useState(false); const [touchUi] = useState(() => detectFeatures().coarse && !detectFeatures().fine); return html`<${Modal} open=${open} onClose=${onClose} title=${t('设置')} micro="SETTINGS" width="7.4rem" actions=${html`<${Button} variant="secondary" icon="book" class="set-guide" onClick=${() => openGuide(0)}>${t('玩法说明')} <${Button} variant="primary" icon="check" onClick=${onClose}>${t('完成')}`}>
${t('语言')}<${MicroLabel}>LANGUAGE <${LangToggle} class="set-lang" />
<${Slider} label=${t('背景音乐')} micro="BGM" icon="play" value=${s.bgm} onInput=${(v) => updateSettings({ bgm: v })} /> <${Slider} label=${t('干员语音')} micro="VOICE" icon="mic" value=${s.voice} onInput=${(v) => updateSettings({ voice: v })} /> <${Slider} label=${t('音效')} micro="SFX" icon="signal" value=${s.sfx} onInput=${(v) => { updateSettings({ sfx: v }); if (!tested) { setTested(true); setTimeout(() => setTested(false), 400); audio.sfx('click'); } }} /> <${Toggle} label=${t('静音')} micro="MUTE" value=${s.muted} onChange=${(v) => updateSettings({ muted: v })} /> <${Toggle} label=${t('显示伤害数字')} micro="DAMAGE NUMBERS" value=${s.damageNumbers} onChange=${(v) => updateSettings({ damageNumbers: v })} />
${t('画面质量')}<${MicroLabel}>QUALITY
${QUALITY.map(([id, label]) => html``)}
${touchUi ? html`

${t('触屏操作:点击单位选中(撤退 / 出售)· 长按单位或卡牌查看详情 · 拖动部署后滑动选择朝向')}

` : html`

${t('快捷键:')}${SHORTCUTS.map(([key, label]) => html`${key} ${t(label)} · `)}${t('右键查看详情')}

`}
`; }