// 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) => `🎵 ${t.title} `).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 … '
+ this._reflectionQuestions(s).map(q => `${q} `).join('') + ' ';
$('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);
}
}