// 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 += `| · ${f.name} | `
+ `· ${f.mult.toFixed(2)} |
`;
}
html += row('= wollten kommen', `${d.demandResult}`, 'sum');
html += `
`;
// 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);
}
}