结构(旧 game/core+engine → 新 engine/ 域): - engine/kernel/ 时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core) - engine/narrative/ legacy/报告/列传/谱系/年轴(原 core 叙事族) - engine/runtime/ World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统) - engine/sim/ Market(未来 WorldSim 同行) - 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层) 验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化) typecheck 0 error
121 lines
3.2 KiB
TypeScript
121 lines
3.2 KiB
TypeScript
// 合成音效:鼓点·钟鸣·纸韵(Web Audio,零素材依赖)
|
||
import { RngHub } from '../game/engine/kernel/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)
|
||
}
|