// DOM-UI: HUD-Kennzahlen, Baumenü, Info-Panels (Gebäude/Besucher), // Ereignis-Karten mit Auto-Pause (inkl. Entscheidungen und Social-Media- // Feed), Meldungs-Log, Zeit-Graph, Ziele, Toasts, Onboarding, Endauswertung. import { BUILDINGS, GROUPS, GOALS, currentAvg, GAME_LEN } from './data.js'; import { telemetry } from './telemetry.js'; import { audio } from './audio.js'; import { music, TRACKS } from './music.js'; const $ = id => document.getElementById(id); const euro = n => Math.round(n).toLocaleString('de-AT'); export class UI { constructor(state, api) { this.state = state; this.api = api; // {setSpeed, getSpeed, setBuildType, selectBuilding, upgradeSelected, demolishSelected, restart, save} this.cardQueue = []; this.cardOpen = false; this.speedBeforeCard = 1; this.collectedCards = []; this.activeBuild = null; this.selectedBuilding = null; this.selectedVisitor = null; this.toastTimer = null; this.gMoney = $('graphMoneyCanvas').getContext('2d'); this.gSat = $('graphSatCanvas').getContext('2d'); this._buildGoals(); this.rebuildMenu(); this._bindTopbar(); this._bindOverlays(); this.updateKPIs(); } // ---------- Baumenü ---------- rebuildMenu() { const wrap = $('buildItems'); wrap.innerHTML = ''; for (const [type, def] of Object.entries(BUILDINGS)) { if (!this.state.unlocked.has(type)) continue; const el = document.createElement('div'); el.className = 'build-item'; el.dataset.type = type; el.innerHTML = ` ${def.emoji} ${def.name}${def.desc} ${euro(def.cost[0])} €`; el.addEventListener('click', () => this._toggleBuild(type, el)); wrap.appendChild(el); } const locked = Object.values(BUILDINGS).filter(d => d.unlockPhase > this.state.phase).length; if (locked > 0) { const hint = document.createElement('div'); hint.className = 'locked-hint'; hint.textContent = `🔒 ${locked} weitere werden im Spielverlauf freigeschaltet.`; wrap.appendChild(hint); } 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(); } _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'); const def = BUILDINGS[type]; this.toast(`${def.emoji} ${def.name} gewählt – tippe auf die Karte. Der Kreis zeigt die Reichweite.`); this.hideInfoPanel(); } 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.budget < def.cost[0]); }); } // ---------- 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 Cockpit um window.location.href='../../schueler.html'; }); $('btnSound').addEventListener('click', e => { // Master-Mute: Effekte, Musik-Player UND Saison-Ambience const on = audio.toggleSound(); music.setMuted(!on); if (!on) audio._stopAmbience?.(); else if (audio.musicOn && !music.playing) audio.startMusic(); e.currentTarget.classList.toggle('muted', !on); telemetry.log('audio_toggle', { kind: 'sound', off: !on }); }); $('btnMusic').addEventListener('click', () => { audio.click(); $('musicPanel').classList.toggle('hidden'); telemetry.log('music_panel', { open: !$('musicPanel').classList.contains('hidden') }); }); this._bindMusic(); $('btnAnim').addEventListener('click', e => { const on = this.api.toggleReduceAnim(); e.currentTarget.classList.toggle('active', on); this.toast(on ? '🐢 Reduzierte Animationen aktiviert.' : '🐢 Animationen wieder aktiviert.'); telemetry.log('reduce_anim', { on }); }); $('btnCamera').addEventListener('click', () => { audio.click(); this.api.resetCamera(); }); $('btnNextWave').addEventListener('click', () => this.api.nextWave()); $('btnHelp').addEventListener('click', () => { audio.click(); this.showOnboarding(); }); $('btnLog').addEventListener('click', () => { audio.click(); this._openLog(); }); } // ---------- Musik-Player ---------- _bindMusic() { const sel = $('muSelect'); sel.innerHTML = TRACKS.map((t, i) => ``).join(''); music.onChange = () => { $('muPlay').textContent = music.playing ? '⏸' : '▶'; sel.value = String(music.idx); }; $('muPlay').addEventListener('click', () => { music.toggle(); telemetry.log('music', { action: 'toggle', track: music.current().title }); }); $('muNext').addEventListener('click', () => { music.next(); telemetry.log('music', { action: 'next', track: music.current().title }); }); $('muPrev').addEventListener('click', () => { music.prev(); telemetry.log('music', { action: 'prev', track: music.current().title }); }); $('muVol').addEventListener('input', e => music.setVolume(Number(e.target.value))); sel.addEventListener('change', () => { music.play(Number(sel.value)); telemetry.log('music', { action: 'select', track: music.current().title }); }); } markSpeed(sp) { document.querySelectorAll('.speed-btn').forEach(b => b.classList.toggle('active', Number(b.dataset.speed) === sp)); } // ---------- Ziele ---------- _buildGoals() { const list = $('goalList'); list.innerHTML = ''; for (const g of GOALS) { const li = document.createElement('li'); li.id = `goal-${g.id}`; li.textContent = g.label; list.appendChild(li); } } // ---------- Kennzahlen + Ruf-Leiste + Wellen-Status ---------- updateKPIs() { const s = this.state; $('clock').textContent = `Welle ${s.phase}/8`; $('seasonBadge').textContent = `${{ spring: '🌸', summer: '☀️', autumn: '🍂', winter: '❄️' }[s.seasonKey]} ${s.season}`; $('kMoney').textContent = `${euro(s.budget)} €`; $('kMoney').classList.toggle('warn', s.budget < 100); $('kEnv').textContent = Math.round(s.env); $('kEnv').classList.toggle('warn', s.env < 45); $('kVisitors').textContent = s.visitors.length; $('kTraffic').textContent = s.traffic; $('kTraffic').classList.toggle('warn', s.traffic > 30); $('kRevenue').textContent = `${euro(s.revenueTotal)} €`; // Ruf: Ø der letzten Abreisen (TD-„Basis-HP"); pulst bei schlechter Abreise const rep = s.recentStars.length ? s.recentStars.reduce((a, b) => a + b, 0) / s.recentStars.length : null; const pct = rep === null ? 60 : Math.round((rep / 5) * 100); $('repVal').textContent = rep === null ? '–' : `${rep.toFixed(1)} ★`; const fill = $('repFill'); fill.style.width = `${pct}%`; fill.classList.toggle('low', rep !== null && rep < 2.5); fill.classList.toggle('mid', rep !== null && rep >= 2.5 && rep < 3.5); if (s.lastBadExit && s.lastBadExit !== this._lastBadShown) { this._lastBadShown = s.lastBadExit; const row = $('repRow'); row.classList.remove('pulse'); void row.offsetWidth; row.classList.add('pulse'); } // Wellen-Status-Banner const ws = $('waveStatus'); if (s.waveStatus === 'pause') { ws.className = 'wave-status pause'; ws.textContent = `🔨 Baupause – Welle ${s.phase} startet in ${Math.ceil(Math.max(0, s.pauseLeft))} s`; } else { const left = s.visitors.filter(v => v.wave === s.phase).length + s.spawnQueue.filter(e => e.type).length; ws.className = 'wave-status running'; ws.textContent = `🌊 Welle ${s.phase} läuft – noch ${left} Gäste`; } $('btnNextWave').disabled = s.ended || (s.phase >= 8 && s.waveStatus === 'running'); $('btnNextWave').textContent = s.waveStatus === 'pause' ? '⏩ Welle jetzt starten' : '⏩ Nächste Welle rufen'; for (const g of GOALS) { $(`goal-${g.id}`)?.classList.toggle('done', g.check(s)); } this.refreshBuildMenu(); this.refreshInfoPanel(); } // ---------- Statistik-Overlay ---------- showStats() { const s = this.state; const rows = (title, items) => { if (!items.length) return ''; const max = Math.max(...items.map(i => i.val), 1); return `

${title}

` + items.map(i => `
${i.label}` + `
` + `${i.disp}
`).join('') + '
'; }; // Zufriedenheit je Gruppe const groups = Object.entries(s.stats.groups) .filter(([, g]) => g.count > 0) .map(([id, g]) => ({ label: `${GROUPS[id].emoji} ${GROUPS[id].name}`, val: g.avg, disp: `${g.avg.toFixed(1)}★`, color: g.avg >= 3.5 ? '#3f8f5e' : g.avg >= 2.5 ? '#e8892b' : '#c0392b' })) .sort((a, b) => b.val - a.val); // Umsatz je Gebäudekategorie const cats = {}; for (const [type, rev] of Object.entries(s.stats.buildingRevenue)) { const c = BUILDINGS[type].cat; cats[c] = (cats[c] || 0) + rev; } const catNames = { gastronomy: '🍽️ Gastronomie', shopping: '🛍️ Handel', nature: '🌿 Natur', play: '🛝 Freizeit', service: '🛎️ Service', culture: '🏛️ Kultur', sport: '⛷️ Sport' }; const revItems = Object.entries(cats).map(([c, v]) => ({ label: catNames[c] || c, val: v, disp: `${euro(v)}€`, color: '#4f7f5e' })).sort((a, b) => b.val - a.val); // Anreise-Mix const tr = s.stats.transports; const trItems = [['foot', '🚶 zu Fuß'], ['bike', '🚴 Rad'], ['bus', '🚌 Bus'], ['car', '🚗 Auto']] .map(([k, l]) => ({ label: l, val: tr[k], disp: `${tr[k]}`, color: k === 'car' ? '#c9531f' : '#2f6ea0' })) .filter(i => i.val > 0); $('statsBody').innerHTML = `

Heatmap der Besucherströme

` + `
` + rows('Zufriedenheit je Gruppe (Ø Sterne)', groups) + rows('Wertschöpfung je Kategorie', revItems) + rows('Anreise der Gäste', trItems) + `

Bilanz

` + `
Umwelt${Math.round(s.env)}
` + `
Verkehr${s.traffic}
` + `
Busgruppen ohne Parkplatz${s.stats.skippedNoParking}
` + `
Gäste gesamt${s.stats.spawned}
`; this._drawHeatmap(); $('statsOverlay').classList.remove('hidden'); telemetry.log('stats_open', {}); } // Heatmap: Kartenbild verkleinert + Besucherstrom-Dichte als Farbflecken _drawHeatmap() { const s = this.state; const cv = $('heatCanvas'); if (!cv) return; const W = 420, H = Math.round(W * s.map.h / s.map.w); const dpr = window.devicePixelRatio || 1; cv.width = W * dpr; cv.height = H * dpr; cv.style.width = W + 'px'; cv.style.height = H + 'px'; const ctx = cv.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); if (!this._heatImg) { this._heatImg = new Image(); } const paint = () => { ctx.clearRect(0, 0, W, H); if (this._heatImg.complete && this._heatImg.naturalWidth) { ctx.globalAlpha = 0.55; ctx.drawImage(this._heatImg, 0, 0, W, H); ctx.globalAlpha = 1; } else { ctx.fillStyle = '#cfe0cf'; ctx.fillRect(0, 0, W, H); } const HC = s.heatCols, HR = s.heatRows; const max = Math.max(...s.heat, 1); const cw = W / HC, ch = H / HR; ctx.globalCompositeOperation = 'source-over'; for (let r = 0; r < HR; r++) { for (let c = 0; c < HC; c++) { const v = s.heat[r * HC + c] / max; if (v < 0.04) continue; const x = (c + 0.5) * cw, y = (r + 0.5) * ch; const rad = ch * 1.6; const g = ctx.createRadialGradient(x, y, 0, x, y, rad); const a = Math.min(0.8, v * 1.1); // grün → gelb → rot je nach Dichte const col = v > 0.66 ? '220,60,40' : v > 0.33 ? '232,150,40' : '90,170,90'; g.addColorStop(0, `rgba(${col},${a})`); g.addColorStop(1, `rgba(${col},0)`); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, rad, 0, 7); ctx.fill(); } } // Legende ctx.font = '10px sans-serif'; ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(47,62,70,0.7)'; ctx.fillText('wenig', 6, H - 6); ctx.textAlign = 'right'; ctx.fillText('viel Verkehr →', W - 6, H - 6); }; if (this._heatImg.src.endsWith(s.map.image)) paint(); else { this._heatImg.onload = paint; this._heatImg.src = s.map.image; paint(); } } // ---------- Info-Panels ---------- showBuildingPanel(b) { this.selectedBuilding = b; this.selectedVisitor = null; this.refreshInfoPanel(); $('infoPanel').classList.remove('hidden'); } showVisitorPanel(v) { this.selectedVisitor = v; this.selectedBuilding = null; this.refreshInfoPanel(); $('infoPanel').classList.remove('hidden'); } hideInfoPanel() { this.selectedBuilding = null; this.selectedVisitor = null; this.api.selectBuilding(null); $('infoPanel').classList.add('hidden'); } refreshInfoPanel() { const s = this.state; if (this.selectedBuilding) { const b = this.selectedBuilding; if (!s.buildings.includes(b)) { this.hideInfoPanel(); return; } const def = BUILDINGS[b.type]; const lvl = b.level - 1; const name = def.levelNames ? def.levelNames[lvl] : def.name; const stars = '★'.repeat(b.level) + '☆'.repeat(def.maxLevel - b.level); const goodFor = Object.entries(def.target) .filter(([, f]) => f >= 1).map(([gid]) => GROUPS[gid]?.emoji).filter(Boolean).join(' '); const rev = s.stats.buildingRevenue[b.type]; const busy = b.construction > 0 ? '🏗️ Baustelle …' : b.upgrading ? '🏗️ Ausbau läuft …' : ''; $('infoTitle').textContent = `${def.emoji} ${name} ${stars}`; $('infoBody').innerHTML = ` ${busy ? `
Status${busy}
` : ''}
Wirkung+${def.sat[lvl]} Zufriedenheit
${def.slow[lvl] ? `
Verweilen−${Math.round(def.slow[lvl] * 100)} % Tempo
` : ''}
Reichweite${def.range[lvl]} Felder
Kapazität${def.capacity[lvl] >= 999 ? 'unbegrenzt' : `${def.capacity[lvl]} Gäste`}
${def.capacity[lvl] < 999 ? `
Auslastung${b.inRange || 0} / ${def.capacity[lvl]}${(b.inRange || 0) > def.capacity[lvl] ? ' – voll!' : ''}
` : ''}
Betriebskosten${def.opCost[lvl]} € / min
${goodFor ? `
Passt für${goodFor}
` : ''} ${rev ? `
Umsatz bisher${euro(rev)} €
` : ''} ${b.infoBoost > 1 ? `
Info-Bonus+${Math.round((b.infoBoost - 1) * 100)} %
` : ''} ${b.synergy > 1 ? `
🔗 Synergie+${Math.round((b.synergy - 1) * 100)} %
` + (b.synergyList || []).map(w => `
✓ ${w}
`).join('') : ''}`; const up = $('btnUpgrade'); if (b.level < def.maxLevel && !busy) { up.classList.remove('hidden'); up.textContent = `⬆️ Ausbauen – ${euro(def.cost[b.level])} €`; up.disabled = s.budget < def.cost[b.level]; } else { up.classList.add('hidden'); } $('btnDemolish').classList.toggle('hidden', b.type === 'naturschutz'); } else if (this.selectedVisitor) { const v = this.selectedVisitor; if (v.exited || !s.visitors.includes(v)) { this.hideInfoPanel(); return; } const g = GROUPS[v.type]; const ratio = Math.max(0, v.satisfaction) / g.satTarget; const stars = Math.max(1, Math.min(5, Math.ceil(ratio * 5))); $('infoTitle').textContent = `${g.emoji} ${g.name}`; $('infoBody').innerHTML = `
Zufriedenheit${Math.round(Math.max(0, v.satisfaction))} / ${g.satTarget}
Budget übrig${euro(g.budget - v.spent)} €
Interessen${g.likes}
Bewertung derzeit${'⭐'.repeat(stars)}
`; $('btnUpgrade').classList.add('hidden'); $('btnDemolish').classList.add('hidden'); } } // ---------- Zwei Verlaufs-Graphen in eigenen Feldern ---------- // Feste 0–20-min-Zeitachse, sichtbare Bodenlinie, alle Daten bleiben. // Links: Budget (€). Rechts: Zufriedenheits-Erfüllung in % (Ziel 100 %, // wie „zerstört" im Tower Defense) + Umweltwert auf derselben 0–100-Skala. drawGraph() { const hist = this.state.history; if (hist.budget.length < 2) return; const bMax = Math.max(600, ...hist.budget); const bMin = Math.min(0, ...hist.budget); this._plotPanel(this.gMoney, { title: '💰 Budget', series: [{ data: hist.budget, color: 'rgb(79,127,94)', fill: true }], vMin: bMin, vMax: bMax, unit: '€', current: `${euro(this.state.budget)} €`, goals: [], t: hist.t, }); this._plotPanel(this.gSat, { title: '😊 Zufriedenheit · 🌲 Umwelt', series: [ { data: hist.satPct, color: 'rgb(232,137,43)', fill: true }, { data: hist.env, color: 'rgb(47,110,160)', fill: false }, ], vMin: 0, vMax: 110, unit: '%', current: `${hist.satPct[hist.satPct.length - 1]} % · ${Math.round(this.state.env)}`, goals: [ { v: 100, label: 'Ziel 100 %' }, { v: 45, label: 'Umwelt-Ziel 45' }, ], t: hist.t, }); } _plotPanel(ctx, o) { const cv = ctx.canvas; const dpr = window.devicePixelRatio || 1; const w = cv.clientWidth, h = cv.clientHeight; if (cv.width !== w * dpr) { cv.width = w * dpr; cv.height = h * dpr; } ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, w, h); const padL = 36, padR = 6, padT = 16, padB = 15; const pw = w - padL - padR, ph = h - padT - padB; const X = i => padL + (o.t[i] / 1200) * pw; const Y = v => padT + ph - ((Math.max(o.vMin, Math.min(v, o.vMax)) - o.vMin) / (o.vMax - o.vMin || 1)) * ph; // Titel + aktueller Wert ctx.font = 'bold 10.5px sans-serif'; ctx.textAlign = 'left'; ctx.fillStyle = '#2f3e46'; ctx.fillText(o.title, 2, 10); ctx.textAlign = 'right'; ctx.fillStyle = o.series[0].color; ctx.fillText(o.current, w - 2, 10); // Skala links (oben/unten) ctx.font = '8.5px sans-serif'; ctx.fillStyle = 'rgba(47,62,70,0.6)'; ctx.textAlign = 'right'; ctx.fillText(`${o.vMax}${o.unit}`, padL - 3, padT + 7); ctx.fillText(`${o.vMin}${o.unit}`, padL - 3, padT + ph); // Ziellinien for (const g of o.goals) { ctx.setLineDash([4, 4]); ctx.strokeStyle = 'rgba(47,62,70,0.35)'; ctx.beginPath(); ctx.moveTo(padL, Y(g.v)); ctx.lineTo(padL + pw, Y(g.v)); ctx.stroke(); ctx.setLineDash([]); ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(47,62,70,0.55)'; ctx.fillText(g.label, padL + 2, Y(g.v) - 2); } // Datenlinien (+ Füllung der ersten Serie) for (const s of o.series) { if (s.fill) { ctx.beginPath(); ctx.moveTo(X(0), padT + ph); s.data.forEach((v, i) => ctx.lineTo(X(i), Y(v))); ctx.lineTo(X(s.data.length - 1), padT + ph); ctx.closePath(); ctx.fillStyle = s.color.replace('rgb', 'rgba').replace(')', ',0.10)'); ctx.fill(); } ctx.strokeStyle = s.color; ctx.lineWidth = 1.8; ctx.beginPath(); s.data.forEach((v, i) => (i ? ctx.lineTo(X(i), Y(v)) : ctx.moveTo(X(i), Y(v)))); ctx.stroke(); ctx.lineWidth = 1; } // BODEN: kräftige Grundlinie + Zeitachse ctx.strokeStyle = 'rgba(47,62,70,0.45)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(padL, padT + ph); ctx.lineTo(padL + pw, padT + ph); ctx.stroke(); ctx.lineWidth = 1; ctx.fillStyle = 'rgba(47,62,70,0.55)'; ctx.textAlign = 'center'; for (const min of [0, 5, 10, 15, 20]) { const x = padL + (min / 20) * pw; ctx.beginPath(); ctx.moveTo(x, padT + ph); ctx.lineTo(x, padT + ph + 3); ctx.stroke(); ctx.fillText(`${min}'`, x, h - 2); } } // ---------- Karten ---------- showCard(card) { // Tempo VOR der ersten Karte merken – auch eine manuelle Pause (0) // wird nach dem Schließen wiederhergestellt. if (!this.cardOpen) this.speedBeforeCard = this.api.getSpeed(); this.cardQueue.push(card); if (!this.cardOpen) this._nextCard(); } _nextCard() { const card = this.cardQueue.shift(); if (!card) return; this.currentCard = card; this.cardOpen = true; this.api.setSpeed(0); this.markSpeed(0); $('cardIcon').textContent = card.icon || 'ℹ️'; $('cardTitle').textContent = card.title || ''; $('cardText').textContent = card.text || ''; const list = $('cardList'); list.innerHTML = ''; list.classList.toggle('hidden', !card.items); if (card.items) { for (const f of card.items) { const li = document.createElement('li'); li.innerHTML = `${'⭐'.repeat(f.stars)}` + `${f.emoji} „${f.text}"`; list.appendChild(li); } $('cardText').textContent = `Ø ${card.avg.toFixed(1)} Sterne → Besucherfaktor ` + `×${card.fame.toFixed(2)} für die nächste Phase.`; } 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 if (!this.state.ended) { this.api.setSpeed(this.speedBeforeCard); this.markSpeed(this.speedBeforeCard); } } _bindOverlays() { $('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'); }); $('infoClose').addEventListener('click', () => { audio.click(); this.hideInfoPanel(); }); $('btnUpgrade').addEventListener('click', () => this.api.upgradeSelected()); $('btnDemolish').addEventListener('click', () => this.api.demolishSelected()); // „Ergebnis speichern"-Button entfernt — Speichern passiert automatisch am // Partie-Ende (submitAssessment in main.js). Listener nur setzen, falls der // Button (Alt-Standalone) doch existiert. { const _bs = $('btnSave'); if (_bs) _bs.addEventListener('click', () => this.api.save()); } $('btnRestart').addEventListener('click', () => this.api.restart()); $('btnStats').addEventListener('click', () => { audio.click(); this.showStats(); }); $('statsClose').addEventListener('click', () => { audio.click(); $('statsOverlay').classList.add('hidden'); }); } _openLog() { const list = $('logList'); list.innerHTML = this.collectedCards.length ? '' : '
  • Noch keine Meldungen.
  • '; for (const c of [...this.collectedCards].reverse()) { const li = document.createElement('li'); const first = (c.text || (c.items ? 'Bewertungen der Gäste' : '')).split('\n')[0]; li.innerHTML = `${c.icon || 'ℹ️'} ${c.title}${first}`; li.addEventListener('click', () => { $('logOverlay').classList.add('hidden'); this.showCard({ ...c, transient: true, choices: null }); }); list.appendChild(li); } $('logOverlay').classList.remove('hidden'); telemetry.log('log_open', { count: this.collectedCards.length }); } // ---------- Endauswertung ---------- showEnd() { const s = this.state; const r = s.result; $('endProfileIcon').textContent = r.profile.icon; $('endProfileName').textContent = r.profile.name; $('endProfileText').textContent = r.profile.text; $('endStars').textContent = `${'⭐'.repeat(Math.round(r.avgStars))} Ø ${r.avgStars.toFixed(1)} Sterne · Gesamtscore ${r.score}/100`; const bars = [ ['Bewertung (35 %)', r.scores.rating, '#e8892b'], ['Wertschöpfung (25 %)', r.scores.revenue, '#4f7f5e'], ['Nachhaltigkeit (20 %)', r.scores.sustain, '#2f6ea0'], ['Besucherlenkung (10 %)', r.scores.lenkung, '#a34a8e'], ['Finanzstabilität (10 %)', r.scores.finance, '#8c6239'], ]; $('endBars').innerHTML = bars.map(([label, v, color]) => `
    ${label}
    ${Math.round(v)}
    `).join(''); $('endGoals').innerHTML = r.goals.map(g => `
  • ${g.done ? '✓' : '✗'} ${g.label}
  • `).join(''); $('endFacts').textContent = `${r.visitors} Besuchergruppen · ${r.rated} Bewertungen · ` + `${euro(r.revenue)} € Wertschöpfung · Umwelt ${r.env} · Budget ${euro(r.budget)} €`; $('endFeedback').innerHTML = r.feedback.map(l => `

    ${l}

    `).join(''); $('endReflect').innerHTML = '

    🧠 Denk mal nach …

    '; $('endOverlay').classList.remove('hidden'); audio.win(); telemetry.log('game_end', { score: r.score, stars: r.avgStars, revenue: r.revenue, env: r.env, budget: r.budget, profile: r.profile.id, }); } // Reflexionsfragen (§24): teils an das Ergebnis angepasst, für die // Nachbesprechung im Unterricht (offene Fragen, keine „richtige" Antwort). _reflectionQuestions(s) { const q = []; const st = s.stats; // stärkste/schwächste Gruppe let best = null, worst = null; for (const [id, g] of Object.entries(st.groups)) { if (g.count < 3) continue; if (!best || g.avg > best.avg) best = { id, ...g }; if (!worst || g.avg < worst.avg) worst = { id, ...g }; } if (worst && worst.avg < 3.2) { q.push(`${GROUPS[worst.id].name} waren am unzufriedensten. Welche Infrastruktur ` + `hätte ihnen geholfen – und warum hast du sie nicht (rechtzeitig) gebaut?`); } if (s.env < 50) { q.push('Der Umweltwert ist gesunken. Welche Entscheidungen haben der Natur ' + 'geschadet? Wie ließe sich Tourismus nachhaltiger gestalten?'); } else { q.push('Du hast die Umwelt gut geschützt. Wo musstest du dafür auf Einnahmen ' + 'verzichten – und war das die richtige Abwägung?'); } if (s.traffic > 25 || st.skippedNoParking > 0) { q.push('Verkehr und Erreichbarkeit waren ein Thema. Wie verändern Parkplatz, ' + 'Bushaltestelle und Radweg, wer überhaupt zu Gast kommt – und mit welchen Folgen?'); } q.push('Warum lohnt sich dieselbe Infrastruktur an einem Standort mehr als an ' + 'einem anderen? Nenne ein Beispiel aus deiner Partie.'); q.push('Wie hat die Saison (Frühling → Winter) verändert, was sich gelohnt hat?'); if (best) { q.push(`Deine Region wurde zur „${s.result.profile.name}". Wolltest du das – ` + 'oder ist es „passiert"? Was würdest du beim nächsten Mal anders planen?'); } return q.slice(0, 5); } // ---------- Onboarding ---------- showOnboarding() { this.showCard({ icon: '🗺️', title: 'So entwickelst du deine Tourismusregion', transient: true, text: '1. Besuchergruppen ziehen über die Wege durch die Region – jede hat ' + 'eigene Interessen (antippen zeigt sie!).\n\n' + '2. Wähle rechts ein Gebäude und baue es NAH AN DEN WEGEN. Der Kreis ' + 'zeigt, wen es erreicht. Antippen: Info + Ausbau.\n\n' + '3. Zufriedene Gäste geben Geld aus und bewerten die Region – gute ' + 'Bewertungen bringen in der nächsten Welle mehr Gäste.\n\n' + '4. Achte auf Saison, Umweltwert und Budget: Betriebskosten laufen ' + 'immer weiter!\n\n' + '5. Gäste kommen in 8 Wellen. Zwischen den Wellen ist Baupause – ' + 'baue und rüste dann auf (Bauen kostet Zeit!). „Nächste Welle rufen" ' + 'startet früher, mit Bonus. Karte bewegen: ' + 'ziehen · Zoom: zwei Finger / Mausrad · ⏸ pausiert.', }); telemetry.log('onboarding_open'); } toast(msg) { const t = $('toast'); t.textContent = msg; t.classList.remove('hidden'); clearTimeout(this.toastTimer); this.toastTimer = setTimeout(() => t.classList.add('hidden'), 3000); } }