Files
Adminator dfb9e80664 Klima: Engine extrahiert + 2D refactored + Drama-Track + Bugfixes
- engine.js als headless Single Source of Truth (KlimaEngine)
- game-2d.html nutzt engine (-515 Zeilen Duplikation)
- rising-pressure.mp3 als Drama-Slot, Auto-Switch bei kritischem State
- state.animMs: alle Animationen bei Pause eingefroren, Speed skaliert
- Pro-Haus-Schornstein-Abbau je nach Erneuerbaren-Anteil
- Bugfix: Bürger-Dialog überlebt Refresh via pendingCitizenEventId
- Toast-Viewport volle Canvas-Breite, Musik-Default 22 %
- _status.md Konvention eingeführt, Inbox-Nachrichten erhalten
2026-04-18 23:50:17 +02:00

166 lines
5.1 KiB
JavaScript

/**
* Klimawächter Audio-Modul
* ------------------------------------------------------------------
* Kleines SFX-System auf Basis von <audio>-Elementen mit Pooling,
* damit dasselbe SFX mehrfach kurz hintereinander abgespielt werden
* kann (z. B. rasche Kauf-Sounds). Persistiert Volume + Mute in
* localStorage, reagiert auf Speed 0 (Pause) indem es nichts abspielt.
*
* Nutzung aus dem Spiel:
* kwAudio.play('ui-click');
* kwAudio.play('build-wind');
* kwAudio.setVolume(0.5); // 0..1
* kwAudio.toggleMute();
*
* Alle Sound-Dateien liegen in ./assets/sounds/<name>.mp3.
*/
(function () {
'use strict';
const BASE = 'assets/sounds/';
const POOL_SIZE = 3; // parallele Instanzen pro Sound (überlappen)
const LS_VOL = 'kw-sfx-volume';
const LS_MUTE = 'kw-sfx-muted';
// Mapping Welt-Event/Key → Sound-Datei. Die Keys werden vom Spiel
// direkt als `play(key)`-Argument übergeben.
const SOUNDS = {
// UI + Meta
'ui-click': 'ui-click.mp3',
'ui-error': 'ui-error.mp3',
'ui-confirm': 'ui-confirm.mp3',
'achievement': 'achievement.mp3',
'warning': 'warning.mp3',
'praise': 'praise.mp3',
'level-won': 'level-won.mp3',
'level-lost': 'level-lost.mp3',
'game-start': 'game-start.mp3',
'demolish': 'demolish.mp3',
// Bau-Sounds
'build-generic': 'build-generic.mp3',
'build-forest': 'build-forest.mp3',
'build-solar': 'build-solar.mp3',
'build-wind': 'build-wind.mp3',
'build-green-roof': 'build-green-roof.mp3',
'build-bikes': 'build-bikes.mp3',
'build-mangrove': 'build-mangrove.mp3',
'build-sand-fill': 'build-sand-fill.mp3',
'build-dike': 'build-dike.mp3',
'build-sea-wall': 'build-sea-wall.mp3',
'build-coal': 'build-coal.mp3',
'build-airport': 'build-airport.mp3',
'build-cloud-seed': 'build-cloud-seed.mp3',
// Ereignis-Sounds
'event-temperature': 'event-temperature.mp3',
'event-flood': 'event-flood.mp3',
'event-vegetation': 'event-vegetation.mp3',
'event-water': 'event-water.mp3',
'event-glacier': 'event-glacier.mp3',
'event-blackout': 'event-blackout.mp3',
'event-power-back': 'event-power-back.mp3',
'event-tourism': 'event-tourism.mp3',
'event-co2': 'event-co2.mp3',
// Dramatik
'drama-paris-missed': 'drama-paris-missed.mp3',
'drama-disaster': 'drama-disaster.mp3',
'drama-game-over': 'drama-game-over.mp3',
};
// Mapping buy(measureId) → sound-key
const BUILD_MAP = {
'forest': 'build-forest',
'solar': 'build-solar',
'wind': 'build-wind',
'green-roof': 'build-green-roof',
'bikes': 'build-bikes',
'mangrove': 'build-mangrove',
'sand-fill': 'build-sand-fill',
'dike': 'build-dike',
'sea-wall': 'build-sea-wall',
'coal': 'build-coal',
'airport': 'build-airport',
'cloud-seed': 'build-cloud-seed',
};
// Sound-Pools (pro Key ein Array von <audio>-Elementen im Round-Robin)
const _pools = {};
const _poolIdx = {};
let _volume = parseFloat(localStorage.getItem(LS_VOL));
if (isNaN(_volume)) _volume = 0.6;
let _muted = localStorage.getItem(LS_MUTE) === '1';
function _getPool(key) {
if (_pools[key]) return _pools[key];
const file = SOUNDS[key];
if (!file) return null;
const arr = [];
for (let i = 0; i < POOL_SIZE; i++) {
const a = new Audio(BASE + file);
a.preload = 'auto';
a.volume = _volume;
arr.push(a);
}
_pools[key] = arr;
_poolIdx[key] = 0;
return arr;
}
function play(key, opts) {
if (_muted) return;
const pool = _getPool(key);
if (!pool) { console.warn('[kwAudio] unbekannt:', key); return; }
const idx = _poolIdx[key];
_poolIdx[key] = (idx + 1) % pool.length;
const a = pool[idx];
try {
a.currentTime = 0;
a.volume = (opts && opts.volume != null) ? opts.volume : _volume;
const p = a.play();
if (p && p.catch) p.catch(() => { /* Autoplay-Policy o.ä. — ignorieren */ });
} catch (e) { /* ignore */ }
}
function playForBuy(measureId) {
const key = BUILD_MAP[measureId] || 'build-generic';
play(key);
}
function setVolume(v) {
_volume = Math.max(0, Math.min(1, v));
localStorage.setItem(LS_VOL, String(_volume));
for (const k in _pools) {
for (const a of _pools[k]) a.volume = _volume;
}
}
function setMuted(m) {
_muted = !!m;
localStorage.setItem(LS_MUTE, _muted ? '1' : '0');
if (_muted) stopAll();
}
function toggleMute() { setMuted(!_muted); return _muted; }
function isMuted() { return _muted; }
function getVolume() { return _volume; }
function stopAll() {
for (const k in _pools) {
for (const a of _pools[k]) { try { a.pause(); a.currentTime = 0; } catch (e) {} }
}
}
// Exportieren
window.kwAudio = {
play,
playForBuy,
setVolume,
getVolume,
setMuted,
isMuted,
toggleMute,
stopAll,
SOUNDS,
};
})();