Add new chat effects and shop items
This commit is contained in:
@@ -13,6 +13,12 @@
|
||||
* fireworks 烟花(发射滑音 + 高频爆炸噪声)
|
||||
* rain 下雨(双层带通白噪声,低音量,与视觉 8s 同步)
|
||||
* snow 下雪(仅五声音阶铃音,无风声)
|
||||
* sakura 樱花(轻柔风铃 + 微风扫过)
|
||||
* meteors 流星(高速掠空呼啸)
|
||||
* gold-rain 金币雨(金属叮当)
|
||||
* hearts 爱心飘落(温暖双音)
|
||||
* confetti 彩带庆典(礼炮碎响 + 清亮点缀)
|
||||
* fireflies 萤火虫(稀疏微光铃音)
|
||||
*/
|
||||
|
||||
const EffectSounds = (() => {
|
||||
@@ -56,6 +62,82 @@ const EffectSounds = (() => {
|
||||
return buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度一个带包络的简单音符。
|
||||
*
|
||||
* @param {AudioContext} ctx
|
||||
* @param {GainNode} masterGain
|
||||
* @param {{delay?:number, duration?:number, freq:number, endFreq?:number, volume?:number, type?:OscillatorType}} options
|
||||
*/
|
||||
function _scheduleTone(ctx, masterGain, options) {
|
||||
const {
|
||||
delay = 0,
|
||||
duration = 0.8,
|
||||
freq,
|
||||
endFreq = freq,
|
||||
volume = 0.18,
|
||||
type = "sine",
|
||||
} = options;
|
||||
|
||||
const t0 = ctx.currentTime + delay;
|
||||
const osc = ctx.createOscillator();
|
||||
const env = ctx.createGain();
|
||||
osc.type = type;
|
||||
osc.frequency.setValueAtTime(freq, t0);
|
||||
osc.frequency.exponentialRampToValueAtTime(
|
||||
Math.max(20, endFreq),
|
||||
t0 + duration,
|
||||
);
|
||||
env.gain.setValueAtTime(Math.max(0.0001, volume), t0);
|
||||
env.gain.exponentialRampToValueAtTime(0.001, t0 + duration);
|
||||
osc.connect(env);
|
||||
env.connect(masterGain);
|
||||
osc.start(t0);
|
||||
osc.stop(t0 + duration + 0.03);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度一段带滤波扫频的噪声音色,用于呼啸、爆裂等环境声。
|
||||
*
|
||||
* @param {AudioContext} ctx
|
||||
* @param {GainNode} masterGain
|
||||
* @param {{delay?:number, duration?:number, startFreq?:number, endFreq?:number, volume?:number, q?:number, filterType?:BiquadFilterType}} options
|
||||
*/
|
||||
function _scheduleNoiseSweep(ctx, masterGain, options) {
|
||||
const {
|
||||
delay = 0,
|
||||
duration = 0.6,
|
||||
startFreq = 2200,
|
||||
endFreq = 500,
|
||||
volume = 0.16,
|
||||
q = 0.8,
|
||||
filterType = "bandpass",
|
||||
} = options;
|
||||
|
||||
const t0 = ctx.currentTime + delay;
|
||||
const src = ctx.createBufferSource();
|
||||
src.buffer = _makeNoise(ctx, duration + 0.15);
|
||||
const filter = ctx.createBiquadFilter();
|
||||
filter.type = filterType;
|
||||
filter.Q.value = q;
|
||||
filter.frequency.setValueAtTime(startFreq, t0);
|
||||
filter.frequency.exponentialRampToValueAtTime(
|
||||
Math.max(60, endFreq),
|
||||
t0 + duration,
|
||||
);
|
||||
|
||||
const env = ctx.createGain();
|
||||
env.gain.setValueAtTime(0.001, t0);
|
||||
env.gain.linearRampToValueAtTime(volume, t0 + duration * 0.18);
|
||||
env.gain.exponentialRampToValueAtTime(0.001, t0 + duration);
|
||||
|
||||
src.connect(filter);
|
||||
filter.connect(env);
|
||||
env.connect(masterGain);
|
||||
src.start(t0);
|
||||
src.stop(t0 + duration + 0.08);
|
||||
}
|
||||
|
||||
// ─── 雷电音效(三层合成,贴近真实雷声)────────────────────────
|
||||
|
||||
/**
|
||||
@@ -392,6 +474,294 @@ const EffectSounds = (() => {
|
||||
};
|
||||
}
|
||||
|
||||
// ─── 樱花 / 流星 / 金币雨 / 爱心 / 彩带 / 萤火虫音效 ────────────
|
||||
|
||||
/**
|
||||
* 启动樱花音效:低音量风铃加轻微风声。
|
||||
*
|
||||
* @returns {Function} 停止函数
|
||||
*/
|
||||
function _startSakura() {
|
||||
const ctx = _getCtx();
|
||||
const master = ctx.createGain();
|
||||
master.gain.value = 0.42;
|
||||
master.connect(ctx.destination);
|
||||
|
||||
const notes = [523.25, 659.25, 783.99, 880];
|
||||
const plan = [0.35, 1.2, 2.1, 3.6, 5.1, 6.8, 8.2];
|
||||
plan.forEach((delay, index) => {
|
||||
_scheduleTone(ctx, master, {
|
||||
delay,
|
||||
duration: 1.8,
|
||||
freq: notes[index % notes.length],
|
||||
endFreq: notes[index % notes.length] * 0.98,
|
||||
volume: 0.11,
|
||||
type: "sine",
|
||||
});
|
||||
});
|
||||
|
||||
_scheduleNoiseSweep(ctx, master, {
|
||||
delay: 0.2,
|
||||
duration: 3.2,
|
||||
startFreq: 1400,
|
||||
endFreq: 500,
|
||||
volume: 0.04,
|
||||
q: 0.35,
|
||||
});
|
||||
_scheduleNoiseSweep(ctx, master, {
|
||||
delay: 4.8,
|
||||
duration: 2.8,
|
||||
startFreq: 1200,
|
||||
endFreq: 420,
|
||||
volume: 0.035,
|
||||
q: 0.3,
|
||||
});
|
||||
|
||||
const endTimer = setTimeout(() => {
|
||||
master.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.8);
|
||||
setTimeout(() => {
|
||||
try {
|
||||
master.disconnect();
|
||||
} catch (_) {}
|
||||
}, 900);
|
||||
}, 9800);
|
||||
|
||||
return () => {
|
||||
clearTimeout(endTimer);
|
||||
try {
|
||||
master.gain.setValueAtTime(0, ctx.currentTime);
|
||||
master.disconnect();
|
||||
} catch (_) {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动流星音效:多段高速呼啸掠空。
|
||||
*
|
||||
* @returns {Function} 停止函数
|
||||
*/
|
||||
function _startMeteors() {
|
||||
const ctx = _getCtx();
|
||||
const master = ctx.createGain();
|
||||
master.gain.value = 0.55;
|
||||
master.connect(ctx.destination);
|
||||
|
||||
[0.4, 1.5, 2.8, 4.2, 5.7, 7.1].forEach((delay) => {
|
||||
_scheduleNoiseSweep(ctx, master, {
|
||||
delay,
|
||||
duration: 0.85,
|
||||
startFreq: 3600,
|
||||
endFreq: 320,
|
||||
volume: 0.14,
|
||||
q: 1.1,
|
||||
});
|
||||
_scheduleTone(ctx, master, {
|
||||
delay: delay + 0.08,
|
||||
duration: 0.55,
|
||||
freq: 1100,
|
||||
endFreq: 420,
|
||||
volume: 0.06,
|
||||
type: "triangle",
|
||||
});
|
||||
});
|
||||
|
||||
const endTimer = setTimeout(() => {
|
||||
try {
|
||||
master.disconnect();
|
||||
} catch (_) {}
|
||||
}, 9300);
|
||||
|
||||
return () => {
|
||||
clearTimeout(endTimer);
|
||||
try {
|
||||
master.disconnect();
|
||||
} catch (_) {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动金币雨音效:连续金属叮当声。
|
||||
*
|
||||
* @returns {Function} 停止函数
|
||||
*/
|
||||
function _startGoldRain() {
|
||||
const ctx = _getCtx();
|
||||
const master = ctx.createGain();
|
||||
master.gain.value = 0.62;
|
||||
master.connect(ctx.destination);
|
||||
|
||||
const notes = [880, 987.77, 1174.66, 1318.51];
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const delay = 0.25 + i * 0.42;
|
||||
const freq = notes[i % notes.length];
|
||||
_scheduleTone(ctx, master, {
|
||||
delay,
|
||||
duration: 0.5,
|
||||
freq,
|
||||
endFreq: freq * 0.92,
|
||||
volume: 0.16,
|
||||
type: "triangle",
|
||||
});
|
||||
_scheduleTone(ctx, master, {
|
||||
delay: delay + 0.02,
|
||||
duration: 0.38,
|
||||
freq: freq * 2.1,
|
||||
endFreq: freq * 1.9,
|
||||
volume: 0.06,
|
||||
type: "sine",
|
||||
});
|
||||
}
|
||||
|
||||
const endTimer = setTimeout(() => {
|
||||
master.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.6);
|
||||
setTimeout(() => {
|
||||
try {
|
||||
master.disconnect();
|
||||
} catch (_) {}
|
||||
}, 700);
|
||||
}, 7600);
|
||||
|
||||
return () => {
|
||||
clearTimeout(endTimer);
|
||||
try {
|
||||
master.gain.setValueAtTime(0, ctx.currentTime);
|
||||
master.disconnect();
|
||||
} catch (_) {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动爱心飘落音效:柔和双音和声。
|
||||
*
|
||||
* @returns {Function} 停止函数
|
||||
*/
|
||||
function _startHearts() {
|
||||
const ctx = _getCtx();
|
||||
const master = ctx.createGain();
|
||||
master.gain.value = 0.5;
|
||||
master.connect(ctx.destination);
|
||||
|
||||
[0.35, 1.5, 2.9, 4.2, 5.8, 7.2].forEach((delay) => {
|
||||
_scheduleTone(ctx, master, {
|
||||
delay,
|
||||
duration: 0.9,
|
||||
freq: 523.25,
|
||||
endFreq: 493.88,
|
||||
volume: 0.12,
|
||||
type: "triangle",
|
||||
});
|
||||
_scheduleTone(ctx, master, {
|
||||
delay: delay + 0.12,
|
||||
duration: 1.05,
|
||||
freq: 659.25,
|
||||
endFreq: 622.25,
|
||||
volume: 0.1,
|
||||
type: "sine",
|
||||
});
|
||||
});
|
||||
|
||||
const endTimer = setTimeout(() => {
|
||||
master.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.8);
|
||||
setTimeout(() => {
|
||||
try {
|
||||
master.disconnect();
|
||||
} catch (_) {}
|
||||
}, 900);
|
||||
}, 9000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(endTimer);
|
||||
try {
|
||||
master.gain.setValueAtTime(0, ctx.currentTime);
|
||||
master.disconnect();
|
||||
} catch (_) {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动彩带庆典音效:礼炮碎响与亮点装饰音。
|
||||
*
|
||||
* @returns {Function} 停止函数
|
||||
*/
|
||||
function _startConfetti() {
|
||||
const ctx = _getCtx();
|
||||
const master = ctx.createGain();
|
||||
master.gain.value = 0.58;
|
||||
master.connect(ctx.destination);
|
||||
|
||||
[0.2, 1.4, 2.8, 4.1, 5.6].forEach((delay) => {
|
||||
_scheduleNoiseSweep(ctx, master, {
|
||||
delay,
|
||||
duration: 0.38,
|
||||
startFreq: 1800,
|
||||
endFreq: 700,
|
||||
volume: 0.2,
|
||||
q: 0.7,
|
||||
});
|
||||
_scheduleTone(ctx, master, {
|
||||
delay: delay + 0.04,
|
||||
duration: 0.42,
|
||||
freq: 1046.5,
|
||||
endFreq: 880,
|
||||
volume: 0.08,
|
||||
type: "square",
|
||||
});
|
||||
});
|
||||
|
||||
const endTimer = setTimeout(() => {
|
||||
try {
|
||||
master.disconnect();
|
||||
} catch (_) {}
|
||||
}, 7200);
|
||||
|
||||
return () => {
|
||||
clearTimeout(endTimer);
|
||||
try {
|
||||
master.disconnect();
|
||||
} catch (_) {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动萤火虫音效:低音量稀疏闪烁音。
|
||||
*
|
||||
* @returns {Function} 停止函数
|
||||
*/
|
||||
function _startFireflies() {
|
||||
const ctx = _getCtx();
|
||||
const master = ctx.createGain();
|
||||
master.gain.value = 0.28;
|
||||
master.connect(ctx.destination);
|
||||
|
||||
[0.6, 1.8, 2.7, 3.9, 5.2, 6.4, 7.6, 8.8].forEach((delay, index) => {
|
||||
_scheduleTone(ctx, master, {
|
||||
delay,
|
||||
duration: 1.4,
|
||||
freq: [659.25, 783.99, 987.77][index % 3],
|
||||
endFreq: [659.25, 783.99, 987.77][index % 3] * 0.99,
|
||||
volume: 0.06,
|
||||
type: "sine",
|
||||
});
|
||||
});
|
||||
|
||||
const endTimer = setTimeout(() => {
|
||||
master.gain.linearRampToValueAtTime(0, ctx.currentTime + 1.0);
|
||||
setTimeout(() => {
|
||||
try {
|
||||
master.disconnect();
|
||||
} catch (_) {}
|
||||
}, 1100);
|
||||
}, 10200);
|
||||
|
||||
return () => {
|
||||
clearTimeout(endTimer);
|
||||
try {
|
||||
master.gain.setValueAtTime(0, ctx.currentTime);
|
||||
master.disconnect();
|
||||
} catch (_) {}
|
||||
};
|
||||
}
|
||||
|
||||
// ─── 公开 API ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -400,7 +770,7 @@ const EffectSounds = (() => {
|
||||
* 当 AudioContext 处于 suspended 状态时,先 resume() 再播放,
|
||||
* 解决页面无用户手势时的自动静音问题(如管理员进房自动烟花)。
|
||||
*
|
||||
* @param {string} type 'lightning' | 'fireworks' | 'rain' | 'snow'
|
||||
* @param {string} type 'lightning' | 'fireworks' | 'rain' | 'snow' | 'sakura' | 'meteors' | 'gold-rain' | 'hearts' | 'confetti' | 'fireflies'
|
||||
*/
|
||||
function play(type) {
|
||||
// 用户开启禁音则跳过
|
||||
@@ -425,6 +795,24 @@ const EffectSounds = (() => {
|
||||
case "snow":
|
||||
_stopFn = _startSnow();
|
||||
break;
|
||||
case "sakura":
|
||||
_stopFn = _startSakura();
|
||||
break;
|
||||
case "meteors":
|
||||
_stopFn = _startMeteors();
|
||||
break;
|
||||
case "gold-rain":
|
||||
_stopFn = _startGoldRain();
|
||||
break;
|
||||
case "hearts":
|
||||
_stopFn = _startHearts();
|
||||
break;
|
||||
case "confetti":
|
||||
_stopFn = _startConfetti();
|
||||
break;
|
||||
case "fireflies":
|
||||
_stopFn = _startFireflies();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user