Files
ChronicleOfTheImmortalClan/src/renderer/ui/sound.ts
T
Hermes Agent 967ef1240d v0.1.8: 万物皆是插件 + 全应用审计修复(60+ 缺陷歼灭)
插件协议:
- CotycPlugin Manifest(dependencies/conflicts/protected/install/uninstall)
- PluginManager:安装序确定性/依赖拒绝/异常回滚/追踪型上下文(卸载全摘钩子)
- 三核心插件常驻(Systems/Data/Events);插件清单/安装移除/启停广播
- EventPoolRegistry 多池合并;facade about/query('plugins')/plugin 订阅
- 设置页插件清单(含卸载按钮、核心保护);examplePlugin 开发范本入仓

全应用审计修复(引擎 39 + UI/主进程 40 双路清单,P0→P2 歼灭):
- P0:单亲 rollRoots 崩/插件事件软锁(findEvent 查池)/fire 覆盖幸存事件/
  facade 未赋值(act 目录 6/24 生效→全量接通)/packOverride 死实现真接管/
  4 能力卡无接线(trib/apprentice/tournament/season)/读档 rowid 方言崩溃/
  buildApprentice 空候选崩/插件安装无回滚/trait 加成从未消费/DEV 双跑破坏时序
- P1:定时器幸存/advance in-flight 闸/battle 暂停/tournament 点将真传/
  史书 memo 依赖/面板 hooks 顺序违规/远征 squad 残留 id/跨档状态残留/
  toast 自清除/读档错误提示/导入后重载世界/pendingEvent 读档回填
- P2:版本三处统一/NewGame 随机收编/学费错误文案/姓氏池去重/
  功法品阶错位(凡阶哨兵)/功法定价对齐/market 白名单校验/
  战利功法去重/回声漏封缄/clock 迭代快照/core 事件池保护/
  渡劫陨落走真突破/寄读 desc 诚实化/doas 死代码清理等 30 项

测试 239→256;金钟罩复验(防御批零漂移)
2026-08-23 09:58:15 +08:00

121 lines
3.2 KiB
TypeScript
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.
// 合成音效:鼓点·钟鸣·纸韵(Web Audio,零素材依赖)
import { RngHub } from '../game/core/rng'
let ctx: AudioContext | null = null
let enabled = true
export function setSoundEnabled(on: boolean): void {
enabled = on
}
export function isSoundEnabled(): boolean {
return enabled
}
function ac(): AudioContext | null {
if (!enabled) return null
try {
if (!ctx) {
const AC = window.AudioContext ?? (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext
ctx = new AC()
}
if (ctx.state === 'suspended') void ctx.resume()
return ctx
} catch {
return null
}
}
function tone(freq: number, dur: number, gain: number, type: OscillatorType = 'sine', when = 0, slideTo?: number): void {
const a = ac()
if (!a) return
const t0 = a.currentTime + when
const o = a.createOscillator()
const g = a.createGain()
o.type = type
o.frequency.setValueAtTime(freq, t0)
if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, t0 + dur)
g.gain.setValueAtTime(0, t0)
g.gain.linearRampToValueAtTime(gain, t0 + 0.012)
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur)
o.connect(g)
g.connect(a.destination)
o.start(t0)
o.stop(t0 + dur + 0.05)
}
function noise(dur: number, gain: number, when = 0, freq = 3200): void {
const a = ac()
if (!a) return
const t0 = a.currentTime + when
const len = Math.floor(a.sampleRate * dur)
const buf = a.createBuffer(1, len, a.sampleRate)
const data = buf.getChannelData(0)
for (let i = 0; i < len; i++) data[i] = (RngHub.audioNoise01() * 2 - 1) * (1 - i / len)
const src = a.createBufferSource()
src.buffer = buf
const f = a.createBiquadFilter()
f.type = 'highpass'
f.frequency.value = freq
const g = a.createGain()
g.gain.setValueAtTime(gain, t0)
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur)
src.connect(f)
f.connect(g)
g.connect(a.destination)
src.start(t0)
}
/** 月度推进:极轻的硖墨声 */
export function sTick(): void {
noise(0.05, 0.015, 0, 5000)
}
/** 开卷 / 纸笺 */
export function sPaper(): void {
noise(0.12, 0.03, 0, 2400)
tone(620, 0.1, 0.015, 'sine', 0.02)
}
/** 好消息:铜钟一声 */
export function sGood(): void {
tone(660, 0.5, 0.06, 'sine')
tone(990, 0.42, 0.03, 'sine', 0.05)
tone(1320, 0.3, 0.018, 'sine', 0.09)
}
/** 坏消息:折磬低响 */
export function sBad(): void {
tone(196, 0.4, 0.05, 'triangle', 0, 130)
tone(131, 0.5, 0.04, 'triangle', 0.04, 82)
}
/** 战报:鼓点急雨 */
export function sWar(): void {
tone(120, 0.16, 0.07, 'sine', 0, 60)
tone(120, 0.16, 0.06, 'sine', 0.16, 60)
tone(160, 0.3, 0.06, 'sine', 0.32, 80)
noise(0.28, 0.02, 0.36, 1500)
}
/** 钟鸣:大事件(碑立/飞升) */
export function sBell(): void {
tone(392, 1.4, 0.06, 'sine')
tone(494, 1.2, 0.035, 'sine', 0.08)
tone(587, 1.0, 0.028, 'sine', 0.16)
}
/** 突破:磬缶清越 */
export function sGong(): void {
tone(440, 0.9, 0.05, 'sine')
tone(660, 0.7, 0.03, 'sine', 0.06)
}
let lastClick = 0
/** 按钮:纸面轻叩(80ms 节流防连点爆 buffer */
export function sClick(): void {
const now = performance.now()
if (now - lastClick < 80) return
lastClick = now
noise(0.045, 0.03, 0, 4200)
}