// UI-Logik: Kennzahlen, Bau-Menü (inkl. Abriss), Event-Karten (mit // Auto-Pause), Meldungs-Log, Graphen, Skipass-Regler, Ziele, Toasts, // Onboarding, Szenenwahl und Topbar-Buttons. import { BUILDINGS, BUILD_INFO, KNOWLEDGE } from './data.js'; import { seasonOfDay, DAYS_PER_YEAR } from './model.js'; import { telemetry } from './telemetry.js'; import { audio } from './audio.js'; import { player, TRACKS } from './player.js'; const $ = id => document.getElementById(id); export class UI { constructor(state, api) { this.state = state; this.api = api; // {setSpeed, getSpeed, setBuildType} this.cardQueue = []; this.cardOpen = false; this.speedBeforeCard = 1; this.collectedCards = []; // Vorlese-Modus: kann von der Lehrperson pro Klasse gesperrt werden this.audioAllowed = window.TT_FEATURES?.audioInfo !== false; this.infoMode = this.audioAllowed ? (localStorage.getItem('tt_infomode') || 'read') : 'read'; this.activeBuild = null; this.toastTimer = null; $('sceneName').textContent = state.scenario.name; this._buildMenu(); this._bindTopbar(); this._initPlayer(); this._bindOverlays(); this._bindPrice(); this._buildGoals(); this.updateKPIs(); } // ---------- Bau-Menü (zeigt nur Freigeschaltetes – kleine Schritte) ---------- _buildMenu() { this.rebuildMenu(); } rebuildMenu() { const wrap = $('buildItems'); wrap.innerHTML = ''; for (const [type, def] of Object.entries(BUILDINGS)) { if (!this.state.unlocked.has(type)) continue; wrap.appendChild(this._buildItem(type, def.emoji, def.name, def.desc, `${def.cost.toLocaleString('de-AT')} €`)); } // Hinweis: Ausbauen & Abreißen laufen jetzt über Klick aufs Gebäude // aktive Auswahl wiederherstellen if (this.activeBuild) { const el = wrap.querySelector(`[data-type="${this.activeBuild}"]`); if (el) el.classList.add('active'); else { this.activeBuild = null; this.api.setBuildType(null); } } this.refreshBuildMenu(); } // Kennzahlen & Panels erscheinen erst, wenn sie relevant werden applyProgress() { const s = this.state; const hasLift = s.unlocked.has('lift') && s.buildings.some(b => b.type === 'lift'); $('kpiRowSnow').classList.toggle('hidden', !s.unlocked.has('lift')); $('priceRow').classList.toggle('hidden', !hasLift); $('priceSlider').classList.toggle('hidden', !hasLift); $('goals').classList.toggle('hidden', !hasLift); $('graphs').classList.toggle('hidden', s.buildings.length < 2); $('kpiRowNature').classList.toggle('hidden', s.buildings.length < 2); $('kpiRowStaff').classList.toggle('hidden', !s.unlocked.has('staffhouse') && s.staffNeeded <= s.staffAvailable); $('quest').classList.toggle('hidden', s.buildings.length < 1 || this.questDone === true); } _buildItem(type, emoji, name, desc, cost) { const el = document.createElement('div'); el.className = 'build-item'; el.dataset.type = type; el.innerHTML = `
${name} ${cost}
${emoji} ${desc}
`; el.addEventListener('click', () => this._toggleBuild(type, el)); el.querySelector('.bi-menu-info').addEventListener('click', e => { e.stopPropagation(); // nicht in den Baumodus wechseln audio.click(); this.showBuildInfo(type); }); return el; } // ℹ im Baumenü: erklärt ein Bauwerk VOR dem Bau – Wirkung, Kosten, // Lage-Regeln und das Lehrplan-Wissen dazu. showBuildInfo(type) { const def = BUILDINGS[type]; const info = BUILD_INFO[type]; const know = (KNOWLEDGE[type] || []) .map(k => `📚 ${k.term}: ${k.text}`).join('\n\n'); const staffLine = def.staff ? ` + ${def.staff} Angestellte` : ''; this.showCard({ id: `menuinfo-${type}`, transient: true, forceModal: true, icon: def.emoji, sound: 'none', title: def.name, text: `${def.desc}\n\nBaukosten: ${def.cost.toLocaleString('de-AT')} € · ` + `laufend: ${def.upkeep} €/Tag${staffLine}.` + (info ? `\n\n${info.text}` : '') + (know ? `\n\n${know}` : ''), }); telemetry.log('menu_info', { type }); } _toggleBuild(type, el) { audio.click(); document.querySelectorAll('.build-item').forEach(i => i.classList.remove('active')); if (this.activeBuild === type) { this.activeBuild = null; } else { this.activeBuild = type; el.classList.add('active'); this.toast(type === 'demolish' ? '🧨 Abriss gewählt – tippe auf ein Gebäude.' : type === 'upgrade' ? '⬆️ Ausbau gewählt – tippe auf ein Gebäude.' : `${BUILDINGS[type].emoji} ${BUILDINGS[type].name} gewählt – tippe auf die Karte zum Bauen.`); } this.api.setBuildType(this.activeBuild); telemetry.log('build_select', { type: this.activeBuild }); } clearBuildSelection() { this.activeBuild = null; this.api.setBuildType(null); document.querySelectorAll('.build-item').forEach(i => i.classList.remove('active')); } refreshBuildMenu() { document.querySelectorAll('.build-item').forEach(el => { const def = BUILDINGS[el.dataset.type]; if (def) el.classList.toggle('disabled', this.state.money < def.cost); }); } // ---------- Topbar ---------- _bindTopbar() { document.querySelectorAll('.speed-btn').forEach(btn => { btn.addEventListener('click', () => { audio.click(); const sp = Number(btn.dataset.speed); this.api.setSpeed(sp); this.markSpeed(sp); telemetry.log('speed', { speed: sp }); }); }); $('btnHome').addEventListener('click', () => { audio.click(); telemetry.log('home_click'); // Der Plattform-Wrapper biegt dieses Ziel auf das gültige Cockpit um // (Schüler → /schueler, Lehrperson → /teacher). Standalone: Fallback. window.location.href='../../schueler.html'; }); $('btnSound').addEventListener('click', e => { const on = audio.toggleSound(); e.currentTarget.classList.toggle('muted', !on); telemetry.log('audio_toggle', { kind: 'sound', off: !on }); }); $('btnMusic').addEventListener('click', () => { audio.click(); $('playerPanel').classList.toggle('hidden'); telemetry.log('player_panel', { open: !$('playerPanel').classList.contains('hidden') }); }); if (!this.audioAllowed) $('btnInfoMode').classList.add('hidden'); $('btnInfoMode').addEventListener('click', e => { audio.click(); this.infoMode = this.infoMode === 'audio' ? 'read' : 'audio'; localStorage.setItem('tt_infomode', this.infoMode); this._syncInfoModeBtn(e.currentTarget); this._stopVoice(); this.toast(this.infoMode === 'audio' ? '🔊 Audio-Modus: Meldungen werden vorgelesen, das Spiel läuft weiter. Entscheidungen pausieren weiterhin.' : '📖 Lese-Modus: Meldungen öffnen sich als Karte, das Spiel pausiert.', 5000); telemetry.log('infomode', { mode: this.infoMode }); }); this._syncInfoModeBtn($('btnInfoMode')); $('btnHelp').addEventListener('click', () => { audio.click(); this.showOnboarding(); }); $('btnLog').addEventListener('click', () => { audio.click(); this._openLog(); }); $('btnReport').addEventListener('click', () => { audio.click(); const cur = this.api.getSpeed(); if (cur > 0) this.speedBeforeCard = cur; this.showReport(); telemetry.log('report_open'); }); } // ---------- Musik-Player ---------- _initPlayer() { const sel = $('trackSelect'); TRACKS.forEach((t, i) => { const o = document.createElement('option'); o.value = i; o.textContent = `♪ ${t.title}`; sel.appendChild(o); }); sel.addEventListener('change', () => { player.select(Number(sel.value)); player.play(); telemetry.log('music', { action: 'select', track: player.current().title }); }); $('plPlay').addEventListener('click', () => { player.toggle(); }); $('plNext').addEventListener('click', () => { player.next(); audio.click(); }); $('plPrev').addEventListener('click', () => { player.prev(); audio.click(); }); const vol = $('plVol'); vol.addEventListener('input', () => player.setVolume(Number(vol.value))); player.onChange = () => { $('plPlay').textContent = player.playing ? '⏸' : '▶'; sel.value = player.idx; $('btnMusic').classList.toggle('muted', !player.playing); }; } syncPlayerUI() { $('plVol').value = player.el ? player.el.volume : 0.09; player.onChange?.(); } _syncInfoModeBtn(btn) { if (!btn) return; btn.textContent = this.infoMode === 'audio' ? '🔊' : '📖'; btn.title = this.infoMode === 'audio' ? 'Audio-Modus: Meldungen werden vorgelesen (umschalten auf Lese-Modus)' : 'Lese-Modus: Meldungen pausieren das Spiel (umschalten auf Audio-Modus)'; } markSpeed(sp) { document.querySelectorAll('.speed-btn').forEach(b => b.classList.toggle('active', Number(b.dataset.speed) === sp)); } // ---------- Skipass-Preis ---------- _bindPrice() { const slider = $('priceSlider'); slider.addEventListener('input', () => { this.state.price = Number(slider.value); $('kPrice').textContent = `${this.state.price} €`; }); slider.addEventListener('change', () => { telemetry.log('price_change', { price: this.state.price }); this.toast(this.state.price >= 35 ? '🎟️ Hoher Preis: mehr Geld pro Gast, aber weniger Gäste.' : this.state.price <= 18 ? '🎟️ Niedriger Preis: viele Gäste, aber wenig Einnahmen.' : '🎟️ Preis angepasst.'); }); } // ---------- Ziele ---------- _buildGoals() { document.querySelector('#goals h3').textContent = this.state.scenario.goalsTitle; const list = $('goalList'); for (const g of this.state.scenario.goals) { const li = document.createElement('li'); li.id = `goal-${g.id}`; li.textContent = g.label; list.appendChild(li); } } updateGoals() { for (const g of this.state.scenario.goals) { $(`goal-${g.id}`)?.classList.toggle('done', g.check(this.state)); } } // ---------- Kennzahlen ---------- updateKPIs() { const s = this.state; $('seasonBadge').textContent = `${s.season} · ${s.year}`; // Jahreskreis-Marker + Wetter mit Vorhersage const dayOfYear = ((s.day - 1) % DAYS_PER_YEAR) / DAYS_PER_YEAR; $('yearMarker').style.left = `${Math.round(dayOfYear * 100)}%`; if (s.weatherToday) { $('weatherNow').textContent = `${s.weatherToday} ${s.temp}°`; $('forecast').textContent = (s.forecast || []) .map(f => `${f.kind}${f.temp}°`).join(' '); } $('kMoney').textContent = `${s.money.toLocaleString('de-AT')} €`; $('kMoney').classList.toggle('warn', s.money < 3000); $('kGuests').textContent = s.guestsToday; // Transparenz: WARUM kamen nicht alle? (Engpass-Diagnose aus dem Modell) const gap = $('kGap'); if (s.bottleneck && s.buildings.length) { gap.textContent = `⚠️ ${s.demandToday} wollten kommen – Engpass: ${s.bottleneck}`; gap.classList.remove('hidden'); } else { gap.classList.add('hidden'); } $('kSnow').textContent = `${Math.round(s.snow)} cm`; $('kSnow').classList.toggle('warn', s.season === 'Winter' && s.snow < 15); $('kSat').textContent = `${s.satisfaction} %`; $('kNature').textContent = `${Math.round(s.nature)} %`; $('kNature').classList.toggle('warn', s.nature < 45); $('kStaff').textContent = `${Math.min(s.staffNeeded, s.staffAvailable)} / ${s.staffNeeded}`; $('kStaff').classList.toggle('warn', s.staffNeeded > s.staffAvailable); this.refreshBuildMenu(); this.updateGoals(); this.applyProgress(); } updateClock() { $('clock').textContent = this.state.dateStr || ''; } // ---------- Graphen (Geld links, Gäste rechts) ---------- drawGraph() { this._plot('graphMoney', this.state.history.money, '#4f7f5e', v => `${v.toLocaleString('de-AT')} €`, true); this._plot('graphCanvas', this.state.history.guests, '#e8892b', v => `${v}`, false); } _plot(id, arr, color, fmt, fill) { const cv = $(id); if (!cv) return; const dpr = window.devicePixelRatio || 1; const w = cv.clientWidth, h = cv.clientHeight; if (!w || !h) return; if (cv.width !== Math.round(w * dpr) || cv.height !== Math.round(h * dpr)) { cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr); } const ctx = cv.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, w, h); if (arr.length < 2) return; // Alles auf die Zeichenfläche begrenzen – nichts kann mehr „ausrauschen" ctx.save(); ctx.beginPath(); ctx.rect(0, 0, w, h); ctx.clip(); const PAD = 8; // fester Rand oben/unten // Jahreszeiten als blasse Bänder + Jahresgrenzen als Linien const SEASON_BG = { Sommer: 'rgba(168,198,95,0.16)', Herbst: 'rgba(216,160,78,0.16)', Winter: 'rgba(169,198,216,0.22)', 'Frühling': 'rgba(181,216,160,0.16)', }; const n = arr.length; const firstDay = this.state.day - n + 1; let runStart = 0; let runSeason = seasonOfDay(firstDay - 1); for (let i = 1; i <= n; i++) { const sn = i < n ? seasonOfDay(firstDay + i - 1) : null; if (sn !== runSeason) { ctx.fillStyle = SEASON_BG[runSeason]; const x0 = (runStart / (n - 1)) * w; const x1 = (Math.min(i, n - 1) / (n - 1)) * w; ctx.fillRect(x0, 0, x1 - x0, h); runStart = i; runSeason = sn; } const d = firstDay + i - 1; if (d > firstDay && (d - 1) % DAYS_PER_YEAR === 0) { const x = ((i - 1) / (n - 1)) * w; ctx.strokeStyle = 'rgba(47,62,70,0.35)'; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); } } ctx.strokeStyle = 'rgba(47,62,70,0.08)'; for (let i = 1; i <= 3; i++) { const y = (h / 4) * i; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } // Eigene Skala mit etwas Kopf-/Fußraum; 0 bleibt sichtbar, wenn nötig. let max = Math.max(...arr); let min = Math.min(...arr, 0); const pad = (max - min) * 0.12 || 1; max += pad; min -= (min < 0 ? pad : 0); const span = max - min || 1; const yOf = v => { const y = (h - PAD) - ((v - min) / span) * (h - 2 * PAD); return Math.max(PAD, Math.min(h - PAD, y)); }; if (min < 0) { const y0 = yOf(0); ctx.strokeStyle = 'rgba(163,50,39,0.45)'; ctx.setLineDash([3, 3]); ctx.beginPath(); ctx.moveTo(0, y0); ctx.lineTo(w, y0); ctx.stroke(); ctx.setLineDash([]); } const pts = arr.map((v, i) => [(i / (n - 1)) * w, yOf(v)]); if (fill) { ctx.beginPath(); ctx.moveTo(pts[0][0], h); pts.forEach(p => ctx.lineTo(p[0], p[1])); ctx.lineTo(pts[n - 1][0], h); ctx.closePath(); ctx.fillStyle = 'rgba(79,127,94,0.12)'; ctx.fill(); } ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.beginPath(); pts.forEach((p, i) => (i === 0 ? ctx.moveTo(p[0], p[1]) : ctx.lineTo(p[0], p[1]))); ctx.stroke(); ctx.lineWidth = 1; ctx.restore(); // Clip aufheben // aktueller Wert oben rechts ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'right'; ctx.fillStyle = color; ctx.fillText(fmt(arr[n - 1]), w - 4, 12); // dynamische Skala: Höchst- und Tiefstwert (und 0-Linie) links beschriften const dMax = Math.max(...arr), dMin = Math.min(...arr); ctx.font = '10px sans-serif'; ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(47,62,70,0.5)'; if (dMax !== arr[n - 1]) { // nicht doppelt mit dem Wert oben rechts ctx.textBaseline = 'top'; ctx.fillText(fmt(dMax), 3, Math.max(1, yOf(dMax))); } ctx.textBaseline = 'bottom'; ctx.fillText(fmt(dMin), 3, Math.min(h - 1, yOf(dMin))); if (min < 0) { ctx.fillStyle = 'rgba(163,50,39,0.7)'; ctx.textBaseline = 'middle'; ctx.fillText(fmt(0), 3, yOf(0)); } ctx.textBaseline = 'alphabetic'; } // ---------- Tagesbericht als Tabelle ---------- showReport() { const d = this.state.reportData; if (!d) return; const eur = v => `${Math.round(v).toLocaleString('de-AT')} €`; const row = (l, r, cls = '') => `${l}${r}`; let html = `
${d.date}` + `${d.weather}${d.snow != null ? ` · ❄️ ${d.snow} cm${d.warmWinter ? ' (warm!)' : ''}` : ''}
`; // Nachfrage: Basiswert × Faktoren = wollten kommen html += `
Wer wollte kommen?
`; html += row(`Grund-Nachfrage (${d.season})`, d.demandBase); for (const f of d.demandFactors) { const bad = f.mult < 0.9; html += `` + ``; } html += row('= wollten kommen', `${d.demandResult}`, 'sum'); html += `
· ${f.name}· ${f.mult.toFixed(2)}
`; // Kapazität / Engpass html += `
Wie viele konnten?
`; html += row('Anreise möglich (Bus & Straße)', d.access); html += row(d.capLabel, d.terrainCap); html += row('→ Gäste gekommen', `${d.guests}${d.overnight ? ` (${d.overnight} ü.N.)` : ''}`, 'sum'); html += `
`; if (d.bottleneck) { html += `
⚠️ Engpass: ${d.bottleneck} – ` + `${d.bottleneckLost} Gäste kamen deshalb nicht. Hier lohnt sich Ausbau!
`; } // Einnahmen / Ausgaben html += `
Einnahmen
`; for (const e of d.einnahmen) html += row(e.label, eur(e.amount)); html += row('Summe', eur(d.income), 'sum'); html += `
`; html += `
Ausgaben
`; for (const e of d.ausgaben) html += row(e.label, `− ${eur(e.amount)}`); html += row('Summe', `− ${eur(d.expenses)}`, 'sum'); html += `
`; html += `
` + `Tagesgewinn${d.net >= 0 ? '+' : ''}${eur(d.net)}
`; for (const h of d.hinweise) html += `
💡 ${h}
`; $('rpBody').innerHTML = html; this.api.setSpeed(0); this.markSpeed(0); $('reportOverlay').classList.remove('hidden'); } // ---------- Event-Karten ---------- // Zwei Info-Modi (Umschalter in der Topbar): // 'read' – wie gehabt: Karte öffnet sich, Spiel pausiert. // 'audio' – Spielfluss läuft weiter: Meldung wird vorgelesen und // erscheint als Info-Zeile oben; Klick darauf (oder 🔔-Log) // öffnet die volle Karte. Entscheidungen (A/B) und wichtige // Meldungen (Sieg, Bank …) bleiben IMMER modal. showCard(card) { if (this.infoMode === 'audio' && !card.choices && !card.important && !card.forceModal) { this._banner(card); return; } this.cardQueue.push(card); if (!this.cardOpen) this._nextCard(); } _banner(card) { if (!card.transient) { this.collectedCards.push(card); const badge = $('logBadge'); badge.textContent = this.collectedCards.length; badge.classList.remove('hidden'); telemetry.log('event_card', { id: card.id || card.title, banner: true }); } const el = $('infoBanner'); el.innerHTML = `${card.icon}${card.title}` + 'antippen für Details'; el.classList.remove('hidden'); el.onclick = () => { el.classList.add('hidden'); this._stopVoice(); this.showCard({ ...card, transient: true, forceModal: true }); }; clearTimeout(this._bannerTimer); this._bannerTimer = setTimeout(() => el.classList.add('hidden'), 9000); this._speakCard(card); } // Vorproduzierte Sprach-Audios (assets/audio-info/.mp3, OpenAI-TTS). // Browser-TTS nur als Not-Fallback, falls eine Datei fehlt. _stopVoice() { if (this._voice) { this._voice.pause(); this._voice = null; } if ('speechSynthesis' in window) speechSynthesis.cancel(); } _speakCard(card) { this._stopVoice(); const id = card.audio || card.id; if (!id) { this._speak(`${card.title}. ${card.text}`); return; } const a = new Audio(`assets/audio-info/${id}.mp3`); a.volume = 0.95; this._voice = a; const fallback = () => { if (this._voice === a) this._speak(`${card.title}. ${card.text}`); }; a.onerror = fallback; a.play().catch(fallback); } // Sprachausgabe (Browser-TTS, deutsche Stimme). null = stoppen. _speak(text) { if (!('speechSynthesis' in window)) return; speechSynthesis.cancel(); if (!text) return; const clean = text .replace(/[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE0F}]/gu, '') // Emojis raus .replace(/[„"«»]/g, '').replace(/\s+/g, ' ').trim(); const u = new SpeechSynthesisUtterance(clean); u.lang = 'de-DE'; u.rate = 1.02; const voices = speechSynthesis.getVoices().filter(v => v.lang?.startsWith('de')); const nice = voices.find(v => /anna|petra|vicki|katja|helena/i.test(v.name)) || voices[0]; if (nice) u.voice = nice; speechSynthesis.speak(u); } _nextCard() { const card = this.cardQueue.shift(); if (!card) return; this.currentCard = card; this.cardOpen = true; const cur = this.api.getSpeed(); if (cur > 0) this.speedBeforeCard = cur; this.api.setSpeed(0); // Auto-Pause, solange die Karte offen ist this.markSpeed(0); $('cardIcon').textContent = card.icon; $('cardTitle').textContent = card.title; $('cardText').textContent = card.text; // Entscheidungs-Karte: zwei Buttons, sonst nur „Weiter" const alt = $('cardAlt'); if (card.choices) { $('cardOk').textContent = card.choices[0].label; $('cardOk').disabled = card.choices[0].disabled || false; alt.textContent = card.choices[1].label; alt.classList.remove('hidden'); } else { $('cardOk').textContent = 'Weiter'; $('cardOk').disabled = false; alt.classList.add('hidden'); } $('cardOverlay').classList.remove('hidden'); if (!card.transient) { const fn = audio[card.sound || 'chime']; if (card.sound !== 'none') (typeof fn === 'function' ? fn : audio.chime).call(audio); this.collectedCards.push(card); const badge = $('logBadge'); badge.textContent = this.collectedCards.length; badge.classList.remove('hidden'); telemetry.log('event_card', { id: card.id || card.title }); } } _closeCard(choiceIdx = null) { const card = this.currentCard; if (card?.choices && choiceIdx !== null) { card.choices[choiceIdx].onPick?.(); } $('cardOverlay').classList.add('hidden'); this.cardOpen = false; this.currentCard = null; if (this.cardQueue.length) { this._nextCard(); } else { this.api.setSpeed(this.speedBeforeCard || 1); this.markSpeed(this.speedBeforeCard || 1); } } // ---------- Gebäude-Inspektion (Klick aufs Gebäude) ---------- showBuildingCard(report, handlers) { this._biHandlers = handlers; this._biReport = report; const roman = ['', 'I', 'II', 'III', 'IV']; $('biIcon').textContent = report.emoji; $('biTitle').textContent = report.name; $('biLevel').textContent = `Ausbaustufe ${roman[report.level]}` + (report.alt !== undefined ? ` · Höhenlage ${report.alt}` : ''); const list = $('biStats'); list.innerHTML = ''; for (const s of report.stats) { const li = document.createElement('li'); li.textContent = s; list.appendChild(li); } $('biCost').innerHTML = `Laufende Kosten: ${report.dailyCost.toLocaleString('de-AT')} €/Tag ` + `(${report.dailyUpkeep} € Instandhaltung + ${report.dailyWage} € Löhne)` + (report.sparflamme ? '
💤 Läuft in dieser Saison auf Sparflamme – reduzierte Kosten.' : ''); // Aktuelle Probleme dieses Gebäudes const probs = $('biProblems'); if (report.problems && report.problems.length) { probs.innerHTML = '
⚠️ Das hakt gerade
' + report.problems.map(p => `
${p}
`).join(''); probs.classList.remove('hidden'); } else { probs.innerHTML = ''; probs.classList.add('hidden'); } // Lehrplan-Wissen (Fachbegriffe im Kontext) const know = $('biKnowledge'); if (report.knowledge && report.knowledge.length) { know.innerHTML = '
📚 Gut zu wissen
' + report.knowledge.map(k => `
${k.term}: ${k.text}
`).join(''); know.classList.remove('hidden'); } else { know.innerHTML = ''; know.classList.add('hidden'); } const up = $('biUpgrade'); if (report.canUpgrade) { const afford = handlers.money >= report.nextCost; up.textContent = `⬆️ Ausbauen auf Stufe ${roman[report.level + 1]} (${report.nextCost.toLocaleString('de-AT')} €)`; up.disabled = !afford; up.classList.remove('hidden'); } else { up.textContent = '★ Maximale Ausbaustufe erreicht'; up.disabled = true; up.classList.remove('hidden'); } // Öko-Umbau (Solar & Co.): einmalig, −20 % Betriebskosten, +Natur const eco = $('biEco'); if (report.eco) { eco.textContent = '🌱 Öko-Betrieb ✓'; eco.disabled = true; } else { eco.textContent = `🌱 Öko-Umbau (${report.ecoCost.toLocaleString('de-AT')} €)`; eco.disabled = handlers.money < report.ecoCost; } this.api.setSpeed(0); this.markSpeed(0); $('buildingOverlay').classList.remove('hidden'); telemetry.log('inspect', { type: report.type, level: report.level }); } _closeBuilding(resume = true) { $('buildingOverlay').classList.add('hidden'); if (resume) { this.api.setSpeed(this.speedBeforeCard || 1); this.markSpeed(this.speedBeforeCard || 1); } } _bindOverlays() { $('biClose').addEventListener('click', () => { audio.click(); this._closeBuilding(); }); $('biUpgrade').addEventListener('click', () => { audio.click(); this._biHandlers?.onUpgrade?.(this._biReport); }); $('biEco').addEventListener('click', () => { audio.click(); this._biHandlers?.onEco?.(this._biReport); }); $('biDemolish').addEventListener('click', () => { audio.click(); this._biHandlers?.onDemolish?.(this._biReport); this._closeBuilding(); }); $('cardOk').addEventListener('click', () => { audio.click(); this._closeCard(this.currentCard?.choices ? 0 : null); }); $('cardAlt').addEventListener('click', () => { audio.click(); this._closeCard(1); }); $('logClose').addEventListener('click', () => { audio.click(); $('logOverlay').classList.add('hidden'); }); $('rpClose').addEventListener('click', () => { audio.click(); $('reportOverlay').classList.add('hidden'); this.api.setSpeed(this.speedBeforeCard || 1); this.markSpeed(this.speedBeforeCard || 1); }); } // ---------- Auftrag (Quest) ---------- updateQuest(quest) { if (!quest) { $('quest').classList.add('hidden'); this.questDone = true; return; } this.currentQuest = quest; $('questIcon').textContent = quest.icon; $('questTitle').textContent = quest.title; $('questDesc').textContent = quest.desc; $('questReward').textContent = `Belohnung: ${quest.reward.toLocaleString('de-AT')} €`; } _openLog() { const list = $('logList'); list.innerHTML = this.collectedCards.length ? '' : '
  • Noch keine Meldungen.
  • '; for (const c of [...this.collectedCards].reverse()) { const li = document.createElement('li'); li.innerHTML = `${c.icon} ${c.title}${c.text.split('\n')[0]}`; li.addEventListener('click', () => { $('logOverlay').classList.add('hidden'); this.showCard({ ...c, transient: true, forceModal: true }); }); list.appendChild(li); } $('logOverlay').classList.remove('hidden'); telemetry.log('log_open', { count: this.collectedCards.length }); } // ---------- Onboarding ---------- showOnboarding() { this.showCard({ icon: '🏔️', transient: true, forceModal: true, title: 'Willkommen! (1/3) – So spielst du', text: 'Rechts ein Gebäude wählen, dann auf die Karte tippen – fertig. ' + 'Nach dem Bau legst du das Werkzeug automatisch ab.\n\n' + 'Oben läuft die Zeit (⏸ bis 4×). Karte bewegen: ziehen · ' + 'Zoomen: zwei Finger oder Mausrad.\n\n' + 'Am Anfang gibt es nur das Gasthaus – alles Weitere schaltet sich ' + 'Schritt für Schritt frei.', }); this.showCard({ icon: '🚶', transient: true, forceModal: true, title: 'Die Gäste (2/3) – dein Kreislauf', text: 'Jede Figur steht für etwa 10 Gäste. Sie kommen mit dem Bus, ' + 'wandern, kehren ein und reisen wieder ab.\n\n' + 'Die Symbole überm Kopf zeigen, was sie vorhaben:\n' + '🍽️ einkehren · 🎿 Ski fahren · 🥾 wandern · 🏖️ baden · 🛷 rodeln\n' + '😋 konsumiert gerade (grüne +€!) · ❄️ „kein Schnee!" · ' + '👋 reist ab · 😞 reist unzufrieden ab\n\n' + 'Wer konsumiert, bringt Geld – wer nicht kommen kann, keins.', }); this.showCard({ icon: '🔍', transient: true, forceModal: true, title: 'Durchblick (3/3) – warum läuft es (nicht)?', text: 'Links stehen deine Kennzahlen. Kommen weniger Gäste als möglich, ' + 'erscheint dort der GRUND (z.B. „Engpass: Anreise").\n\n' + 'Der 📋 Tagesbericht rechnet dir jeden Tag im Detail vor. ' + 'Beim ersten Bau jedes Gebäudes erklärt dir eine Karte, was es ' + 'bringt und was es kostet.\n\n' + 'Der Graph unten zeigt die Jahreszeiten farbig – das blaue Band ist ' + 'die Skisaison (Oktober bis Ostern).\n\n' + 'Ab und zu taucht ein Ereignis oder eine Entscheidung auf – in Ruhe, ' + 'das Spiel pausiert dabei. Diese Anleitung öffnest du jederzeit wieder ' + 'über ℹ oben rechts.', }); telemetry.log('onboarding_open'); } toast(msg, dur = 2600) { const t = $('toast'); t.textContent = msg; t.classList.remove('hidden'); clearTimeout(this.toastTimer); this.toastTimer = setTimeout(() => t.classList.add('hidden'), dur); } }