v0.1.2: 宗族气韵(职事/悟道/谱系/碑录/庆典/飞升/音效)

- 职事任命:长老(+修炼)/供奉(+战力)/执事(+坊市)/掌教(+闭关) 每人加成叠加,上限管理
- 功法悟道:修习积悟性点,小成/大成逐级+战力,大境界突破传道于直系晚辈
- 宗族谱系页:世代分带 + 夫妻同卷 + 子孙连枝,点节点开详情,先人簿可查
- 功德碑:宗主辞世即勒石,宗祠页碑录永存
- 百年庆典 + 飞升之路事件链(留世坐镇/仙风入体 或 云游飞升/宗族蒙荫)
- 合成音效(WebAudio):鼓点/钟鸣/纸韵/磬响,设置页开关
- 单测 29→38;typecheck/build 通过;GUI 冒烟受限于 WSLg 掉线(SMOKE-TIMEOUT 优雅退出)
This commit is contained in:
2026-08-23 08:28:49 +08:00
parent 6b15a97d0c
commit fefbe1f1d7
22 changed files with 864 additions and 8 deletions
+115
View File
@@ -0,0 +1,115 @@
// 合成音效:鼓点·钟鸣·纸韵(Web Audio,零素材依赖)
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] = (Math.random() * 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)
}
/** 按钮:纸面轻叩 */
export function sClick(): void {
noise(0.045, 0.03, 0, 4200)
}