v0.1.2: 宗族气韵(职事/悟道/谱系/碑录/庆典/飞升/音效)
- 职事任命:长老(+修炼)/供奉(+战力)/执事(+坊市)/掌教(+闭关) 每人加成叠加,上限管理 - 功法悟道:修习积悟性点,小成/大成逐级+战力,大境界突破传道于直系晚辈 - 宗族谱系页:世代分带 + 夫妻同卷 + 子孙连枝,点节点开详情,先人簿可查 - 功德碑:宗主辞世即勒石,宗祠页碑录永存 - 百年庆典 + 飞升之路事件链(留世坐镇/仙风入体 或 云游飞升/宗族蒙荫) - 合成音效(WebAudio):鼓点/钟鸣/纸韵/磬响,设置页开关 - 单测 29→38;typecheck/build 通过;GUI 冒烟受限于 WSLg 掉线(SMOKE-TIMEOUT 优雅退出)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user