Tourismustal: Komplett-Integration (Sim + Wrapper + SQL + Lehrer-Backend)

Sim nach App/sims/tourismustal umgezogen (Source of Truth), game.html mit
Injection-Marker. Neu in der Sim: 5 Sommergebaeude (Bikepark mit Flowtrails,
Hochseilgarten, Fahrradverleih als Verstaerker, Aussichtsplattform,
Erlebnisspielplatz), Rettungsstation mit Notarzthubschrauber, Ausbau-
Bauanimation, Event-Pacing, Schulden-Notbremse (Bank-Zwangsverkauf),
Berater-Tipps (Ruecklagen), Sommer-Trend-Mechanik (Serfaus-Effekt) und
Wissens-Bausteine mit echten DACH-Tourismusdaten (QUELLEN.md).

Plattform: PHP-Wrapper mit base-Tag + Session-Mode, Modul-Detailseite,
module_info (beta) + Glossar (14 Begriffe inkl. Leichter Sprache + Quellen)
+ Lehrplan-Anker AT/DE/CH an neuer Kompetenz tourismus-raumentwicklung.
Lehrer-Backend: Benchmark-Formel, Live-Cockpit-Spalten, needsHelp-Heuristik,
Modul-Highlights. Strategie-Testharness (tests/strategien.mjs) belegt:
Panzer-Rush wird liquidiert, Ruecklagen zahlen sich aus.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 02:45:08 +02:00
parent eb565d4c01
commit 0a2ebf46b4
55 changed files with 6820 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
// Klang: KI-generierte Samples (ElevenLabs, assets/sounds/*.mp3) mit
// WebAudio abgespielt; fehlt eine Datei, greift ein kleiner Synth-Fallback.
// Dazu saisonale Atmosphäre (Sommerwiese/Winterwind) als Loop.
// AudioContext entsteht erst nach der ersten Nutzer-Geste (iOS/Safari).
const SAMPLES = ['click', 'build', 'demolish', 'success', 'error',
'card', 'milestone', 'win', 'amb_birds', 'amb_cow', 'amb_cowbell', 'amb_wind'];
export const audio = {
ctx: null,
soundOn: true,
musicOn: true,
_buffers: {}, // Name → AudioBuffer
_raw: {}, // Name → ArrayBuffer (vor dem Decodieren)
_season: 'Sommer',
// MP3s sofort laden (ohne AudioContext), decodiert wird bei ensure()
preload() {
for (const name of SAMPLES) {
fetch(`assets/sounds/${name}.mp3`)
.then(r => (r.ok ? r.arrayBuffer() : null))
.then(buf => { if (buf) { this._raw[name] = buf; this._decode(name); } })
.catch(() => {});
}
},
_decode(name) {
if (!this.ctx || !this._raw[name] || this._buffers[name]) return;
this.ctx.decodeAudioData(this._raw[name].slice(0))
.then(b => {
this._buffers[name] = b;
})
.catch(() => {});
},
ensure() {
if (!this.ctx) {
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return false;
this.ctx = new AC();
for (const name of Object.keys(this._raw)) this._decode(name);
}
if (this.ctx.state === 'suspended') this.ctx.resume();
return true;
},
// ---------- Effekte ----------
play(name, gain = 0.5) {
if (!this.soundOn || !this.ensure()) return;
const buf = this._buffers[name];
if (buf) {
const src = this.ctx.createBufferSource();
const g = this.ctx.createGain();
g.gain.value = gain;
src.buffer = buf;
src.connect(g).connect(this.ctx.destination);
src.start();
} else {
this._synthFallback(name);
}
},
click() { this.play('click', 0.35); },
success() { this.play('success', 0.5); },
error() { this.play('error', 0.45); },
chime() { this.play('card', 0.45); },
card() { this.chime(); },
build() { this.play('build', 0.55); },
demolish() { this.play('demolish', 0.55); },
milestone() { this.play('milestone', 0.55); },
win() { this.play('win', 0.6); },
_tone(freq, dur, type = 'sine', gain = 0.08, delay = 0) {
const t0 = this.ctx.currentTime + delay;
const osc = this.ctx.createOscillator();
const g = this.ctx.createGain();
osc.type = type;
osc.frequency.value = freq;
g.gain.setValueAtTime(0, t0);
g.gain.linearRampToValueAtTime(gain, t0 + 0.02);
g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
osc.connect(g).connect(this.ctx.destination);
osc.start(t0);
osc.stop(t0 + dur + 0.05);
},
_synthFallback(name) {
switch (name) {
case 'click': this._tone(660, 0.08, 'triangle', 0.05); break;
case 'success': case 'build': case 'milestone': case 'win':
this._tone(523, 0.12, 'triangle', 0.07);
this._tone(784, 0.18, 'triangle', 0.07, 0.09);
break;
case 'error': case 'demolish': this._tone(180, 0.22, 'sawtooth', 0.05); break;
default: this._tone(880, 0.3, 'sine', 0.05);
}
},
// ---------- Gelegentliche Naturlaute (statt Dauer-Ambience) ----------
// Alle 1025 s ein kurzer Laut passend zur Saison: Vögel, Kuhglocken,
// ein Muhen im Winter meist nur ein Windhauch.
_ambTimer: null,
setSeason(season) {
this._season = season;
},
_pickAmbience() {
const r = Math.random();
if (this._season === 'Winter') {
return r < 0.6 ? 'amb_wind' : r < 0.85 ? 'amb_cowbell' : 'amb_birds';
}
return r < 0.45 ? 'amb_birds' : r < 0.75 ? 'amb_cowbell' : 'amb_cow';
},
_scheduleAmbience() {
this._ambTimer = setTimeout(() => {
if (this.soundOn && this.ctx) {
const name = this._pickAmbience();
const buf = this._buffers[name];
if (buf) {
const src = this.ctx.createBufferSource();
const g = this.ctx.createGain();
g.gain.value = 0.22;
src.buffer = buf;
src.connect(g).connect(this.ctx.destination);
src.start();
}
}
this._scheduleAmbience();
}, 10000 + Math.random() * 15000);
},
startMusic() {
if (!this.ensure()) return;
clearTimeout(this._ambTimer);
this._scheduleAmbience();
},
_stopAmbience() {
clearTimeout(this._ambTimer);
this._ambTimer = null;
},
toggleMusic() {
this.musicOn = !this.musicOn;
if (this.musicOn) this.startMusic(); else this._stopAmbience();
return this.musicOn;
},
toggleSound() {
this.soundOn = !this.soundOn;
return this.soundOn;
},
};
audio.preload();