Files
SoundGame/web/engine/settings.js
T
SoundGame Dev 79dffac01e guide层v1: 全局向导交互层落地(状态签名earcon/turn cue/静默看门狗/纠错) + 6游戏接线 + 8句向导语音 + 测试全绿
- 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断言)+资产终检全绿
2026-08-30 22:46:05 +08:00

143 lines
4.6 KiB
JavaScript
Raw 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.
/**
* 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;