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
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Klimawächter · Sound-Preview</title>
|
||||
<link rel="stylesheet" href="../../../assets/fonts/inter.css">
|
||||
<link rel="stylesheet" href="../../../assets/css/design-system.css">
|
||||
<style>
|
||||
body { overflow: auto; height: auto; min-height: 100vh; background: var(--ggs-bg); padding: 32px; }
|
||||
h1 { color: var(--ggs-fjord-dark); font-size: 28px; margin-bottom: 8px; }
|
||||
.lead { color: var(--ggs-text-muted); margin-bottom: 24px; max-width: 700px; line-height: 1.5; }
|
||||
.group { margin-bottom: 28px; }
|
||||
.group h2 {
|
||||
color: var(--ggs-fjord-dark); font-size: 16px; text-transform: uppercase;
|
||||
letter-spacing: 0.06em; margin-bottom: 12px; border-bottom: 2px solid var(--ggs-border);
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.card {
|
||||
background: var(--ggs-white); border: 1px solid var(--ggs-border);
|
||||
border-radius: 10px; padding: 12px 14px;
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
}
|
||||
.card h3 { font-size: 13px; font-weight: 800; color: var(--ggs-fjord-dark); margin: 0; }
|
||||
.card .meta { font-size: 11px; color: var(--ggs-text-muted); }
|
||||
.card .prompt { font-size: 11px; color: var(--ggs-text); line-height: 1.4; font-style: italic; }
|
||||
.card audio { width: 100%; margin-top: 4px; }
|
||||
.card.playing { border-color: var(--ggs-moss); box-shadow: 0 2px 8px rgba(90,138,94,0.3); }
|
||||
.toolbar {
|
||||
position: sticky; top: 0; background: var(--ggs-bg);
|
||||
padding: 10px 0; border-bottom: 1px solid var(--ggs-border);
|
||||
margin-bottom: 24px; z-index: 10;
|
||||
display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
|
||||
}
|
||||
.toolbar label { font-size: 13px; color: var(--ggs-text); display: flex; align-items: center; gap: 6px; }
|
||||
.toolbar input[type=range] { width: 140px; }
|
||||
.toolbar .count { font-size: 12px; color: var(--ggs-text-muted); margin-left: auto; }
|
||||
.btn {
|
||||
padding: 6px 14px; border: 1.5px solid var(--ggs-fjord); background: var(--ggs-white);
|
||||
color: var(--ggs-fjord-dark); border-radius: 6px; font-weight: 600; cursor: pointer;
|
||||
font-family: inherit; font-size: 13px;
|
||||
}
|
||||
.btn:hover { background: var(--ggs-fjord-light); }
|
||||
.btn.primary { background: var(--ggs-fjord); color: #fff; }
|
||||
.btn.primary:hover { background: var(--ggs-fjord-dark); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>🔊 Klimawächter — Sound-Preview</h1>
|
||||
<p class="lead">
|
||||
Alle 32 SFX aus <code>assets/sounds/</code>. Klick auf einen Play-Button, um zu hören.
|
||||
Falls ein Sound nicht passt: Dateinamen merken, ich kann den Prompt feintunen und
|
||||
nur diesen einen neu generieren (<code>python generate-sounds.py <name> --force</code>).
|
||||
</p>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn primary" id="btn-play-all">▶ Alle nacheinander</button>
|
||||
<button class="btn" id="btn-stop">⏹ Stop</button>
|
||||
<label>Lautstärke <input type="range" id="vol" min="0" max="1" step="0.05" value="0.7"></label>
|
||||
<span class="count" id="count">Lädt …</span>
|
||||
</div>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<script>
|
||||
const GROUPS = {
|
||||
'UI + Meta': ['ui-click', 'ui-error', 'ui-confirm', 'achievement', 'warning', 'praise', 'level-won', 'level-lost', 'game-start', 'demolish'],
|
||||
'Bau (Klima-positiv)': ['build-forest', 'build-solar', 'build-wind', 'build-green-roof', 'build-bikes', 'build-mangrove'],
|
||||
'Bau (Küstenschutz)': ['build-sand-fill', 'build-dike', 'build-sea-wall'],
|
||||
'Bau (Klima-negativ / Schein)': ['build-coal', 'build-airport', 'build-cloud-seed', 'build-generic'],
|
||||
'Ereignisse': ['event-temperature', 'event-flood', 'event-vegetation', 'event-water', 'event-glacier', 'event-blackout', 'event-power-back', 'event-tourism', 'event-co2'],
|
||||
'Dramatik (wenn es kippt)': ['drama-paris-missed', 'drama-disaster', 'drama-game-over'],
|
||||
};
|
||||
|
||||
let DATA = { sounds: [] };
|
||||
let currentAudio = null;
|
||||
|
||||
async function load() {
|
||||
const res = await fetch('sounds-list.json');
|
||||
DATA = await res.json();
|
||||
render();
|
||||
}
|
||||
|
||||
function findMeta(file) {
|
||||
return DATA.sounds.find(s => s.file === file + '.mp3');
|
||||
}
|
||||
|
||||
function render() {
|
||||
const root = document.getElementById('root');
|
||||
let html = '';
|
||||
let total = 0;
|
||||
for (const [groupName, files] of Object.entries(GROUPS)) {
|
||||
html += '<div class="group"><h2>' + groupName + ' (' + files.length + ')</h2><div class="grid">';
|
||||
for (const file of files) {
|
||||
const meta = findMeta(file);
|
||||
const prompt = meta ? meta.prompt : '';
|
||||
const dur = meta ? meta.duration : '?';
|
||||
html += `
|
||||
<div class="card" data-file="${file}">
|
||||
<h3>${file}.mp3</h3>
|
||||
<div class="meta">Dauer: ${dur} s</div>
|
||||
<div class="prompt">${prompt}</div>
|
||||
<audio controls preload="none" src="../assets/sounds/${file}.mp3"></audio>
|
||||
</div>`;
|
||||
total++;
|
||||
}
|
||||
html += '</div></div>';
|
||||
}
|
||||
root.innerHTML = html;
|
||||
document.getElementById('count').textContent = total + ' Sounds';
|
||||
|
||||
// Volumen global setzen
|
||||
const vol = document.getElementById('vol');
|
||||
document.querySelectorAll('audio').forEach(a => {
|
||||
a.volume = parseFloat(vol.value);
|
||||
a.addEventListener('play', () => {
|
||||
document.querySelectorAll('audio').forEach(o => { if (o !== a) { o.pause(); o.currentTime = 0; } });
|
||||
a.closest('.card').classList.add('playing');
|
||||
currentAudio = a;
|
||||
});
|
||||
a.addEventListener('pause', () => a.closest('.card').classList.remove('playing'));
|
||||
a.addEventListener('ended', () => a.closest('.card').classList.remove('playing'));
|
||||
});
|
||||
vol.addEventListener('input', () => {
|
||||
document.querySelectorAll('audio').forEach(a => a.volume = parseFloat(vol.value));
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btn-play-all').addEventListener('click', () => {
|
||||
const all = Array.from(document.querySelectorAll('audio'));
|
||||
let i = 0;
|
||||
const next = () => {
|
||||
if (i >= all.length) return;
|
||||
const a = all[i++];
|
||||
a.currentTime = 0;
|
||||
a.play();
|
||||
a.addEventListener('ended', next, { once: true });
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
document.getElementById('btn-stop').addEventListener('click', () => {
|
||||
document.querySelectorAll('audio').forEach(a => { a.pause(); a.currentTime = 0; });
|
||||
});
|
||||
|
||||
load();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user