Tourismustal: Audio-Modus fuer Meldungen + eigene Mega-Motive
- Umschalter Topbar (Lese-/Audio-Modus, persistiert): Im Audio-Modus laeuft das Spiel weiter, Meldungen werden per Browser-TTS (deutsche Stimme) vorgelesen und erscheinen als antippbare Info-Zeile oben; Klick oder 🔔-Log oeffnet die volle Karte. Entscheidungen (A/B) und wichtige Karten (Sieg, Zwischenbilanz, Bank) bleiben immer modal. - Megabauwerke: 9 eigene Sprites (mehr Elemente statt vergroesserter Fenster/Tueren - glaubwuerdige Proportionen), zentriert, ~80 % der bisherigen Darstellungsgroesse; Fallback normales Motiv x1,4. Rodelbahn aus der Merge-Liste (dynamische Vektor-Bahn). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ export class UI {
|
||||
this.cardOpen = false;
|
||||
this.speedBeforeCard = 1;
|
||||
this.collectedCards = [];
|
||||
this.infoMode = localStorage.getItem('tt_infomode') || 'read';
|
||||
this.activeBuild = null;
|
||||
this.toastTimer = null;
|
||||
|
||||
@@ -140,6 +141,18 @@ export class UI {
|
||||
$('playerPanel').classList.toggle('hidden');
|
||||
telemetry.log('player_panel', { open: !$('playerPanel').classList.contains('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._speak(null);
|
||||
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', () => {
|
||||
@@ -182,6 +195,14 @@ export class UI {
|
||||
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));
|
||||
@@ -436,11 +457,60 @@ export class UI {
|
||||
}
|
||||
|
||||
// ---------- 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 = `<span class="ib-icon">${card.icon}</span><b>${card.title}</b>`
|
||||
+ '<span class="ib-hint">antippen für Details</span>';
|
||||
el.classList.remove('hidden');
|
||||
el.onclick = () => {
|
||||
el.classList.add('hidden');
|
||||
this._speak(null); // Vorlesen stoppen
|
||||
this.showCard({ ...card, transient: true, forceModal: true });
|
||||
};
|
||||
clearTimeout(this._bannerTimer);
|
||||
this._bannerTimer = setTimeout(() => el.classList.add('hidden'), 9000);
|
||||
this._speak(`${card.title}. ${card.text}`);
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -605,7 +675,7 @@ export class UI {
|
||||
li.innerHTML = `${c.icon} <b>${c.title}</b><small>${c.text.split('\n')[0]}</small>`;
|
||||
li.addEventListener('click', () => {
|
||||
$('logOverlay').classList.add('hidden');
|
||||
this.showCard({ ...c, transient: true });
|
||||
this.showCard({ ...c, transient: true, forceModal: true });
|
||||
});
|
||||
list.appendChild(li);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user