- engine/guide.js 运行时合成8个签名earcon(方法论L2硬参数),off档零定时器,插播queue不顶对白 - settings: guide三档(full/standard/off默认full) + 语速2.2视障档;audio: isVoicePlaying()/setRateBoostEnabled() 清两个已知问题 - 6游戏按schemes§2接线: storyline看门狗+无效手势纠错、simon免扣重听看门狗、escape turn cue+提前卡关提示、rhythm gd_answer/gd_miss+结算看门狗+快进豁免、space首撞指令提示+结算看门狗、rpg格挡纠错+60s催促+状态签名 - 大厅设置面板+向导文档(14-guide-layer.md)+00-global-ux §6;R5台词表补4条一致性 - 试玩转写含向导事件;6 playtest(guide=off断言零改动)+guide.test(34断言)+资产终检全绿
143 lines
4.6 KiB
JavaScript
143 lines
4.6 KiB
JavaScript
/**
|
||
* settings.js — 全局用户设置(纯音频游戏合集共享)。
|
||
*
|
||
* 四项设置:
|
||
* | 键 | 取值 | 默认 | 作用 |
|
||
* |--------|-------------------------------------|----------|----------------------------------------|
|
||
* | voice | 'male' \| 'female' | 'female' | voice 层加载 audio/voice/<gender>/ 下资产 |
|
||
* | speed | 1.0 \| 1.35 \| 1.7 \| 2.2 | 1.0 | voice 层 source.playbackRate(语速;2.2 为视障档) |
|
||
* | bgm | 1.0 \| 0.3 \| 0 | 1.0 | BGM 层增益;0 = 不加载不播放 BGM |
|
||
* | guide | 'full' \| 'standard' \| 'off' | 'full' | 向导帮助等级(engine/guide.js 实时读取)|
|
||
*
|
||
* localStorage 持久化(key: 'soundgame.settings'),同域下大厅与所有游戏
|
||
* 页面天然共享;跨标签页修改通过 'storage' 事件同步给订阅者。
|
||
*
|
||
* 用法:
|
||
* import { settings } from '../engine/settings.js';
|
||
* settings.get(); // -> { voice, speed, bgm, guide }(副本)
|
||
* settings.set({ speed: 1.35 }); // 部分更新,持久化并通知订阅者
|
||
* const off = settings.subscribe((s, changed) => { ... }); // off() 退订
|
||
*
|
||
* 游戏侧一般**不需要**直接用本模块:audio.js 已自动应用 voice/speed/bgm,
|
||
* 向导层(engine/guide.js)实时读 guide。需要自定义响应(如改 UI)才 subscribe。
|
||
*/
|
||
|
||
const STORAGE_KEY = 'soundgame.settings';
|
||
|
||
/** @typedef {{ voice: 'male'|'female', speed: number, bgm: number, guide: 'full'|'standard'|'off' }} SoundGameSettings */
|
||
|
||
/** @type {SoundGameSettings} */
|
||
const DEFAULTS = { voice: 'female', speed: 1.0, bgm: 1.0, guide: 'full' };
|
||
|
||
const VALID = {
|
||
voice: ['male', 'female'],
|
||
speed: [1.0, 1.35, 1.7, 2.2],
|
||
bgm: [1.0, 0.3, 0],
|
||
guide: ['full', 'standard', 'off'],
|
||
};
|
||
|
||
/** @type {SoundGameSettings} */
|
||
let current = { ...DEFAULTS };
|
||
|
||
/** @type {Set<(s: SoundGameSettings, changed: Partial<SoundGameSettings>) => void>} */
|
||
const listeners = new Set();
|
||
|
||
function isValid(key, value) {
|
||
return VALID[key] && VALID[key].includes(value);
|
||
}
|
||
|
||
/** 从 localStorage 读取并合并默认值(非法值逐项丢弃,保证永不炸)。 */
|
||
function load() {
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY);
|
||
if (!raw) return;
|
||
const obj = JSON.parse(raw);
|
||
for (const key of Object.keys(DEFAULTS)) {
|
||
if (isValid(key, obj[key])) current[key] = obj[key];
|
||
}
|
||
} catch (_) {
|
||
// localStorage 不可用(隐私模式)或数据损坏 → 用默认值
|
||
}
|
||
}
|
||
|
||
function persist() {
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(current));
|
||
} catch (_) {
|
||
// 写不进去就算了,内存值仍然生效
|
||
}
|
||
}
|
||
|
||
function notify(changed) {
|
||
const snapshot = { ...current };
|
||
for (const fn of listeners) {
|
||
try {
|
||
fn(snapshot, changed);
|
||
} catch (e) {
|
||
console.error('[settings] 订阅者回调异常:', e);
|
||
}
|
||
}
|
||
}
|
||
|
||
load();
|
||
|
||
// 跨标签页同步(大厅和游戏页同时开着时)
|
||
window.addEventListener('storage', (e) => {
|
||
if (e.key !== STORAGE_KEY) return;
|
||
const before = { ...current };
|
||
load();
|
||
const changed = {};
|
||
for (const key of Object.keys(DEFAULTS)) {
|
||
if (before[key] !== current[key]) changed[key] = current[key];
|
||
}
|
||
if (Object.keys(changed).length > 0) notify(changed);
|
||
});
|
||
|
||
export const settings = {
|
||
/**
|
||
* 读取当前设置(返回副本,改它不影响内部状态)。
|
||
* @returns {SoundGameSettings}
|
||
*/
|
||
get() {
|
||
return { ...current };
|
||
},
|
||
|
||
/**
|
||
* 部分更新设置。非法键/值被忽略(console.warn),合法项立即持久化
|
||
* 并通知所有订阅者。
|
||
* @param {Partial<SoundGameSettings>} patch
|
||
* @returns {SoundGameSettings} 更新后的完整设置
|
||
*/
|
||
set(patch) {
|
||
const changed = {};
|
||
for (const [key, value] of Object.entries(patch || {})) {
|
||
if (!isValid(key, value)) {
|
||
console.warn('[settings] 忽略非法设置项:', key, value);
|
||
continue;
|
||
}
|
||
if (current[key] !== value) {
|
||
current[key] = value;
|
||
changed[key] = value;
|
||
}
|
||
}
|
||
if (Object.keys(changed).length > 0) {
|
||
persist();
|
||
notify(changed);
|
||
}
|
||
return { ...current };
|
||
},
|
||
|
||
/**
|
||
* 订阅设置变更。回调签名 (snapshot, changed)。
|
||
* audio.js 靠它实现实时生效(BGM 立即调增益、voice 下一句生效)。
|
||
* @param {(s: SoundGameSettings, changed: Partial<SoundGameSettings>) => void} fn
|
||
* @returns {() => void} 退订函数
|
||
*/
|
||
subscribe(fn) {
|
||
listeners.add(fn);
|
||
return () => listeners.delete(fn);
|
||
},
|
||
};
|
||
|
||
export default settings;
|