Deploy, skill and result lines come from the operators' official voice slots; a newer line takes the channel over (stale callbacks of the replaced line no longer release it), and the result line is spoken by an operator of the player's own battle. Contributed by @Convey123.
81 lines
4.4 KiB
JavaScript
81 lines
4.4 KiB
JavaScript
// 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.
|
|
|
|
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';
|
|
|
|
/** 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<ReturnType<typeof sanitizeSettings>>} 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`<label class="set-row">
|
|
<span class="set-row__label"><${Icon} name=${icon} />${label}<${MicroLabel}>${micro}<//></span>
|
|
<input class="set-range" type="range" min="0" max="100" step="5" value=${pct} style=${`--pct:${pct}%`}
|
|
onInput=${(e) => onInput(Number(e.currentTarget.value) / 100)} />
|
|
<span class="set-row__val num">${pct}</span>
|
|
</label>`;
|
|
}
|
|
|
|
function Toggle({ label, micro, value, onChange }) {
|
|
return html`<div class="set-row">
|
|
<span class="set-row__label">${label}<${MicroLabel}>${micro}<//></span>
|
|
<button type="button" class=${`set-toggle${value ? ' is-on' : ''}`} role="switch" aria-checked=${value ? 'true' : 'false'}
|
|
onClick=${() => onChange(!value)}><i></i><span>${value ? '开启' : '关闭'}</span></button>
|
|
</div>`;
|
|
}
|
|
|
|
const QUALITY = [['high', '高'], ['medium', '中'], ['low', '低']];
|
|
|
|
/**
|
|
* 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="设置" micro="SETTINGS" width="7.4rem"
|
|
actions=${html`<${Button} variant="secondary" icon="book" class="set-guide" onClick=${() => openGuide(0)}>玩法说明<//>
|
|
<${Button} variant="primary" icon="check" onClick=${onClose}>完成<//>`}>
|
|
<div class="set-list">
|
|
<${Slider} label="背景音乐" micro="BGM" icon="play" value=${s.bgm} onInput=${(v) => updateSettings({ bgm: v })} />
|
|
<${Slider} label="干员语音" micro="VOICE" icon="mic" value=${s.voice} onInput=${(v) => updateSettings({ voice: v })} />
|
|
<${Slider} label="音效" 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="静音" micro="MUTE" value=${s.muted} onChange=${(v) => updateSettings({ muted: v })} />
|
|
<${Toggle} label="显示伤害数字" micro="DAMAGE NUMBERS" value=${s.damageNumbers} onChange=${(v) => updateSettings({ damageNumbers: v })} />
|
|
<div class="set-row">
|
|
<span class="set-row__label">画面质量<${MicroLabel}>QUALITY<//></span>
|
|
<div class="set-seg" role="radiogroup">
|
|
${QUALITY.map(([id, label]) => html`<button key=${id} type="button" role="radio" aria-checked=${s.quality === id ? 'true' : 'false'}
|
|
class=${s.quality === id ? 'is-on' : ''} onClick=${() => updateSettings({ quality: id })}>${label}</button>`)}
|
|
</div>
|
|
</div>
|
|
${touchUi
|
|
? html`<p class="set-hint">触屏操作:点击单位选中(撤退 / 出售)· 长按单位或卡牌查看详情 · 拖动部署后滑动选择朝向</p>`
|
|
: html`<p class="set-hint">快捷键:<kbd>R</kbd> 刷新 · <kbd>F</kbd> 冻结 · <kbd>D</kbd> 升级 · <kbd>Q</kbd> 撤退选中干员 · <kbd>X</kbd> 出售选中干员 · <kbd>Space</kbd> 准备就绪 · <kbd>Esc</kbd> 关闭弹窗 · 右键查看详情</p>`}
|
|
</div>
|
|
<//>`;
|
|
}
|