Add new chat effects and shop items

This commit is contained in:
2026-04-12 16:48:58 +08:00
parent 33a3e5d118
commit 70cb170f2c
25 changed files with 1707 additions and 60 deletions
+389 -1
View File
@@ -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;
}