- 新增 web/ 纯音频 HTML5 平台:共享引擎(手势/WebAudio 三层/设置/震动)、 Node 零依赖服务器(静态 + 注册登录 + 进度存档 API) - 游戏:剑与声(RPG 新作)、声命线、暗室(重设计)、音序回响(重设计)、 节拍行者(跟随领奏重构)、盲航(同向闪避重构);声东击西删除 - 全局功能:男声/女声双套 TTS 资产、三档语速、BGM 音量三档、 震动反馈语言(10 种震型)、服务器进度同步 - 资产:899 ogg + 899 m4a(Qwen3-TTS 双性别语音、Stable Audio 3 音效、 Mureka BGM 4+3 首),终检零缺失 - 文档:docs/design/ 13 篇设计审查文档、docs/audio/ 资产管线、 web/server/README.md API 文档
141 lines
4.3 KiB
JavaScript
141 lines
4.3 KiB
JavaScript
/**
|
||
* settings.js — 全局用户设置(纯音频游戏合集共享)。
|
||
*
|
||
* 三项设置:
|
||
* | 键 | 取值 | 默认 | 作用 |
|
||
* |--------|-----------------------|----------|----------------------------------------|
|
||
* | voice | 'male' \| 'female' | 'female' | voice 层加载 audio/voice/<gender>/ 下资产 |
|
||
* | speed | 1.0 \| 1.35 \| 1.7 | 1.0 | voice 层 source.playbackRate(语速) |
|
||
* | bgm | 1.0 \| 0.3 \| 0 | 1.0 | BGM 层增益;0 = 不加载不播放 BGM |
|
||
*
|
||
* localStorage 持久化(key: 'soundgame.settings'),同域下大厅与所有游戏
|
||
* 页面天然共享;跨标签页修改通过 'storage' 事件同步给订阅者。
|
||
*
|
||
* 用法:
|
||
* import { settings } from '../engine/settings.js';
|
||
* settings.get(); // -> { voice, speed, bgm }(副本)
|
||
* settings.set({ speed: 1.35 }); // 部分更新,持久化并通知订阅者
|
||
* const off = settings.subscribe((s, changed) => { ... }); // off() 退订
|
||
*
|
||
* 游戏侧一般**不需要**直接用本模块:audio.js 已自动应用三项设置
|
||
* (voice 路径/语速/BGM 增益)。需要自定义响应(如改 UI)才 subscribe。
|
||
*/
|
||
|
||
const STORAGE_KEY = 'soundgame.settings';
|
||
|
||
/** @typedef {{ voice: 'male'|'female', speed: number, bgm: number }} SoundGameSettings */
|
||
|
||
/** @type {SoundGameSettings} */
|
||
const DEFAULTS = { voice: 'female', speed: 1.0, bgm: 1.0 };
|
||
|
||
const VALID = {
|
||
voice: ['male', 'female'],
|
||
speed: [1.0, 1.35, 1.7],
|
||
bgm: [1.0, 0.3, 0],
|
||
};
|
||
|
||
/** @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;
|