dfb9e80664
- 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
166 lines
5.1 KiB
JavaScript
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,
|
|
};
|
|
})();
|