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:
Moritz Graf
2026-09-24 21:11:12 +02:00
parent 8a0b65c3fa
commit fbc4158016
17 changed files with 1272 additions and 629 deletions
+144 -52
View File
@@ -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) {