feat(sasch40): v4.0 full-screen mobile conversion, Hinter den Bergen room with Liars Dice, beat-only concert audio, and updated art
This commit is contained in:
@@ -244,37 +244,119 @@ class SoundEngine {
|
||||
if (!this.ctx) return
|
||||
|
||||
const now = this.ctx.currentTime
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
|
||||
if (!success) {
|
||||
// Missed clunk / muted buzz
|
||||
// Missed clunk / muted buzz ('plink')
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
osc.type = 'sawtooth'
|
||||
osc.frequency.setValueAtTime(90, now)
|
||||
gain.gain.setValueAtTime(0.12, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12)
|
||||
osc.frequency.setValueAtTime(110, now)
|
||||
osc.frequency.exponentialRampToValueAtTime(45, now + 0.08)
|
||||
gain.gain.setValueAtTime(0.15, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08)
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
osc.start(now)
|
||||
osc.stop(now + 0.08)
|
||||
} else {
|
||||
// Resonant lute string twang
|
||||
const freqs = [293.66, 392.00, 587.33] // D4, G4, D5
|
||||
const freq = freqs[lane % 3] || 440
|
||||
osc.type = 'triangle'
|
||||
osc.frequency.setValueAtTime(freq, now)
|
||||
// Resonant medieval lute chords (2 harmonic notes for rich sound)
|
||||
const chordRoots = [
|
||||
[293.66, 440.00], // D4 + A4 (Saite 1 - Deep resonant D power)
|
||||
[392.00, 587.33], // G4 + D5 (Saite 2 - Bright G medieval fifth)
|
||||
[440.00, 659.25] // A4 + E5 (Saite 3 - High piercing A rock lute)
|
||||
]
|
||||
const notes = chordRoots[lane % 3] || [392.00, 587.33]
|
||||
|
||||
gain.gain.setValueAtTime(0.28, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.32)
|
||||
notes.forEach((freq, idx) => {
|
||||
if (!this.ctx) return
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
osc.type = 'triangle'
|
||||
osc.frequency.setValueAtTime(freq, now + idx * 0.012)
|
||||
|
||||
gain.gain.setValueAtTime(0.24, now + idx * 0.012)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.38)
|
||||
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
osc.start(now + idx * 0.012)
|
||||
osc.stop(now + 0.38)
|
||||
})
|
||||
}
|
||||
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
|
||||
osc.start(now)
|
||||
osc.stop(now + (success ? 0.32 : 0.12))
|
||||
}
|
||||
|
||||
// SFX: Liar's Dice - Shake dice in leather cup
|
||||
public playDiceShake() {
|
||||
if (this.isMuted) return
|
||||
this.initCtx()
|
||||
if (!this.ctx) return
|
||||
|
||||
const now = this.ctx.currentTime
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const t = now + i * 0.06
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
osc.type = 'square'
|
||||
osc.frequency.setValueAtTime(180 + Math.random() * 120, t)
|
||||
gain.gain.setValueAtTime(0.12, t)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.04)
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
osc.start(t)
|
||||
osc.stop(t + 0.04)
|
||||
}
|
||||
}
|
||||
|
||||
// SFX: Liar's Dice - Cup slam on tavern wood table
|
||||
public playCupSlam() {
|
||||
if (this.isMuted) return
|
||||
this.initCtx()
|
||||
if (!this.ctx) return
|
||||
|
||||
const now = this.ctx.currentTime
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
osc.type = 'sawtooth'
|
||||
osc.frequency.setValueAtTime(110, now)
|
||||
osc.frequency.exponentialRampToValueAtTime(30, now + 0.12)
|
||||
gain.gain.setValueAtTime(0.35, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15)
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
osc.start(now)
|
||||
osc.stop(now + 0.15)
|
||||
}
|
||||
|
||||
// SFX: Gaoliang Shot Pour / Clink
|
||||
public playGaoliangShot() {
|
||||
if (this.isMuted) return
|
||||
this.initCtx()
|
||||
if (!this.ctx) return
|
||||
|
||||
const now = this.ctx.currentTime
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
osc.type = 'sine'
|
||||
osc.frequency.setValueAtTime(1760, now) // A6 glass clink
|
||||
osc.frequency.setValueAtTime(2637, now + 0.04) // E7
|
||||
gain.gain.setValueAtTime(0.2, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4)
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
osc.start(now)
|
||||
osc.stop(now + 0.4)
|
||||
}
|
||||
|
||||
private bgmMode: 'full' | 'beat_only' = 'full'
|
||||
|
||||
// 135 BPM Medieval Techno Chiptune Beat
|
||||
public startBGM() {
|
||||
if (this.isMuted || this.bgmPlaying) return
|
||||
public startBGM(mode: 'full' | 'beat_only' = 'full') {
|
||||
this.bgmMode = mode
|
||||
if (this.isMuted) return
|
||||
if (this.bgmPlaying) {
|
||||
// If already playing, just update mode
|
||||
return
|
||||
}
|
||||
this.initCtx()
|
||||
this.bgmPlaying = true
|
||||
|
||||
@@ -340,47 +422,50 @@ class SoundEngine {
|
||||
hatNode.start(now)
|
||||
}
|
||||
|
||||
// Bassline (sawtooth filtered)
|
||||
const bassFreq = bassline[this.step % bassline.length]
|
||||
if (bassFreq > 0) {
|
||||
const bassOsc = this.ctx.createOscillator()
|
||||
const bassGain = this.ctx.createGain()
|
||||
const bassFilter = this.ctx.createBiquadFilter()
|
||||
// ONLY PLAY BASSLINE & MELODY IF NOT IN BEAT-ONLY MODE!
|
||||
if (this.bgmMode === 'full') {
|
||||
// Bassline (sawtooth filtered)
|
||||
const bassFreq = bassline[this.step % bassline.length]
|
||||
if (bassFreq > 0) {
|
||||
const bassOsc = this.ctx.createOscillator()
|
||||
const bassGain = this.ctx.createGain()
|
||||
const bassFilter = this.ctx.createBiquadFilter()
|
||||
|
||||
bassOsc.type = 'sawtooth'
|
||||
bassOsc.frequency.setValueAtTime(bassFreq, now)
|
||||
bassOsc.type = 'sawtooth'
|
||||
bassOsc.frequency.setValueAtTime(bassFreq, now)
|
||||
|
||||
bassFilter.type = 'lowpass'
|
||||
bassFilter.frequency.setValueAtTime(450, now)
|
||||
bassFilter.type = 'lowpass'
|
||||
bassFilter.frequency.setValueAtTime(450, now)
|
||||
|
||||
bassGain.gain.setValueAtTime(0.12, now)
|
||||
bassGain.gain.exponentialRampToValueAtTime(0.001, now + 0.2)
|
||||
bassGain.gain.setValueAtTime(0.12, now)
|
||||
bassGain.gain.exponentialRampToValueAtTime(0.001, now + 0.2)
|
||||
|
||||
bassOsc.connect(bassFilter)
|
||||
bassFilter.connect(bassGain)
|
||||
bassGain.connect(this.ctx.destination)
|
||||
bassOsc.connect(bassFilter)
|
||||
bassFilter.connect(bassGain)
|
||||
bassGain.connect(this.ctx.destination)
|
||||
|
||||
bassOsc.start(now)
|
||||
bassOsc.stop(now + 0.2)
|
||||
}
|
||||
bassOsc.start(now)
|
||||
bassOsc.stop(now + 0.2)
|
||||
}
|
||||
|
||||
// Medieval Lute Lead Melody
|
||||
const melFreq = melody[this.step % melody.length]
|
||||
if (melFreq > 0) {
|
||||
const melOsc = this.ctx.createOscillator()
|
||||
const melGain = this.ctx.createGain()
|
||||
// Medieval Lute Lead Melody
|
||||
const melFreq = melody[this.step % melody.length]
|
||||
if (melFreq > 0) {
|
||||
const melOsc = this.ctx.createOscillator()
|
||||
const melGain = this.ctx.createGain()
|
||||
|
||||
melOsc.type = 'square'
|
||||
melOsc.frequency.setValueAtTime(melFreq, now)
|
||||
melOsc.type = 'square'
|
||||
melOsc.frequency.setValueAtTime(melFreq, now)
|
||||
|
||||
melGain.gain.setValueAtTime(0.08, now)
|
||||
melGain.gain.exponentialRampToValueAtTime(0.001, now + 0.18)
|
||||
melGain.gain.setValueAtTime(0.08, now)
|
||||
melGain.gain.exponentialRampToValueAtTime(0.001, now + 0.18)
|
||||
|
||||
melOsc.connect(melGain)
|
||||
melGain.connect(this.ctx.destination)
|
||||
melOsc.connect(melGain)
|
||||
melGain.connect(this.ctx.destination)
|
||||
|
||||
melOsc.start(now)
|
||||
melOsc.stop(now + 0.18)
|
||||
melOsc.start(now)
|
||||
melOsc.stop(now + 0.18)
|
||||
}
|
||||
}
|
||||
|
||||
this.step++
|
||||
@@ -390,6 +475,13 @@ class SoundEngine {
|
||||
playLoop()
|
||||
}
|
||||
|
||||
public setBGMMode(mode: 'full' | 'beat_only') {
|
||||
this.bgmMode = mode
|
||||
if (!this.bgmPlaying && !this.isMuted) {
|
||||
this.startBGM(mode)
|
||||
}
|
||||
}
|
||||
|
||||
public stopBGM() {
|
||||
this.bgmPlaying = false
|
||||
if (this.bgmTimer) {
|
||||
|
||||
Reference in New Issue
Block a user