edu-boardgame-generator/js/tour.js

278 lines
11 KiB
JavaScript

/**
* js/tour.js — Tour durch Schritt 3 (Spielfeld bauen)
* Triggert beim ersten Aufruf von goTo(2). Figur (cfg.figure) führt durch alle Bereiche.
* Während der Tour ist die Seite VOLLSTÄNDIG GESPERRT — nur die Buttons in der
* Sprechblase und ein Klick auf den Text (für Skip-Tippvorgang) sind aktiv.
*/
(function () {
// ── Storyboard ──
// selector: CSS-Selector des Anker-Elements (null = kein Anker, Figur in Mitte)
// text: Sprechblasen-Text (\n für Absatz)
// side: 'left'|'right'|'top'|'bottom'|'auto' — wo die Figur am Anker landet
const TOUR_STEPS = [
{
id: 'welcome', selector: null,
text: "Hi! Ich bin {fig} und führe dich kurz durch dein Spielfeld. Los geht's!\n\nDer Board Game Emulator befindet sich übrigens noch in der Entwicklung. Wenn dir auffällt, dass was nicht funktioniert, gib das kurz deinem Lehrer oder deiner Lehrerin weiter — vielleicht kann er/sie dir direkt helfen.",
},
{
id: 'bewegung', selector: '[data-tour="bewegung"]', side: 'bottom',
text: "Hier wählst du, wie sich deine Figur bewegt — würfeln für Spannung, oder genau ein Feld pro Runde für mehr Kontrolle.",
},
{
id: 'spielmodus', selector: '[data-tour="spielmodus"]', side: 'bottom',
text: "Willst du lieber mit Leben spielen, die du verlieren kannst? Oder Punkte sammeln beim Gewinnen?",
},
{
id: 'modus-counter', selector: '[data-tour="modus-counter"]:not([style*="display: none"])', side: 'bottom',
text: "Wie viele Leben jeder bekommt — oder mit welcher Punktzahl gestartet wird — stellst du hier ein.",
},
{
id: 'code-pane', selector: '#codePane', side: 'left',
text: "Hier rechts siehst du den Programmcode deines Spiels — und zwar live! Während du dein Spiel baust, kannst du direkt mitverfolgen, wie der Code im Hintergrund entsteht. Das ist übrigens Python — eine sehr häufig benutzte Programmiersprache.",
},
{
id: 'konsequenz-sieg', selector: '[data-tour="konsequenz-sieg"]', side: 'bottom',
text: "Wenn ein Mini-Game gewonnen wird, darf was Tolles passieren: ein paar Felder vor, Bonuspunkte oder nochmal würfeln.",
},
{
id: 'konsequenz-niederlage', selector: '[data-tour="konsequenz-niederlage"]', side: 'bottom',
text: "Und wenn jemand verliert? Vielleicht ein paar Felder zurück, ein Leben weniger oder eine Runde aussetzen.",
},
{
id: 'fieldcount', selector: '[data-tour="fieldcount"]', side: 'right',
text: "Wie groß soll dein Spielfeld sein? Zwischen 6 und 30 Feldern — je mehr, desto länger das Spiel.",
},
{
id: 'palette', selector: '[data-tour="palette"]', side: 'right',
text: "Das hier sind deine Bausteine: 12 Mini-Games und Erzähltexte. Die ziehst du gleich aufs Spielfeld.",
},
{
id: 'world-reroll', selector: '#wbReroll', side: 'right',
text: "Das ist dein Spielfeld! Mit '🎲 Welt neu generieren' würfelst du eine neue Welt aus — so lange, bis dir der Pfad gefällt.",
},
{
id: 'world-canvas', selector: '#worldCanvas', side: 'right',
text: "Zieh die Bausteine einfach auf die runden Felder hier. Klick auf ein belegtes Feld, wenn du es wieder leeren willst.",
},
{
id: 'world-lock', selector: '#wbLock', side: 'right',
text: "Wenn dein Spielbrett perfekt ist, klick auf '✓ Spielbrett verwenden' — dann bleibt es so.",
},
{
id: 'next', selector: '#s3next', side: 'left',
text: "Fertig? Dann geht's mit 'Weiter' zum Quiz. Viel Spaß beim Bauen!",
},
];
// ── State ──
let stepIdx = 0;
let typingTimer = null;
let typingFull = '';
let typingPos = 0;
let active = false;
let layoutHandler = null;
let suppressAnim = false; // Scroll/Resize → keine Position-Transition (sonst hinkt's hinterher)
// ── Public API ──
window.tourStart = function () {
if (active) return;
active = true;
stepIdx = 0;
const root = document.getElementById('tourRoot');
if (!root) return;
root.style.display = '';
// Figur-Emoji aus ST.figure
const figId = typeof ST !== 'undefined' && ST.figure;
const fig = (typeof FIGURES !== 'undefined' && FIGURES.find(f => f.id === figId)) || null;
const figEmoji = fig ? fig.e : '🎮';
document.getElementById('tourFigure').textContent = figEmoji;
// Layout neu-bauen bei Resize/Scroll (capture: fängt auch Scrolls auf inneren Containern wie #editorPane)
layoutHandler = () => { suppressAnim = true; layoutCurrent(); };
window.addEventListener('resize', layoutHandler);
document.addEventListener('scroll', layoutHandler, { passive: true, capture: true });
showStep(0);
};
window.tourNext = function () {
if (!active) return;
if (typingTimer) { skipTypewriter(); return; }
if (stepIdx >= TOUR_STEPS.length - 1) { tourFinish(); return; }
showStep(stepIdx + 1);
};
window.tourPrev = function () {
if (!active) return;
if (stepIdx > 0) showStep(stepIdx - 1);
};
window.tourSkipConfirm = function () {
if (!active) return;
appConfirm('Tour wirklich beenden?\n\nDu kannst sie später jederzeit nochmal starten.',
() => tourFinish(),
{ okText: 'Ja, beenden', cancelText: 'Weiter ansehen', icon: '🚪' }
);
};
function tourFinish() {
if (!active) return;
active = false;
if (typingTimer) { clearInterval(typingTimer); typingTimer = null; }
const root = document.getElementById('tourRoot');
if (root) root.style.display = 'none';
if (layoutHandler) {
window.removeEventListener('resize', layoutHandler);
document.removeEventListener('scroll', layoutHandler, { capture: true });
layoutHandler = null;
}
try { localStorage.setItem('tourSchritt3Done', '1'); } catch (e) {}
}
// ── Steps ──
function showStep(idx) {
stepIdx = idx;
const step = TOUR_STEPS[idx];
// Progress-Indikator
const prog = document.getElementById('tourBubbleProgress');
if (prog) prog.textContent = `${idx + 1} / ${TOUR_STEPS.length}`;
// Buttons aktualisieren
const btnNext = document.getElementById('tourBtnNext');
if (btnNext) btnNext.textContent = (idx >= TOUR_STEPS.length - 1) ? '✓ Fertig' : 'Weiter →';
const btnBack = document.getElementById('tourBtnBack');
if (btnBack) btnBack.style.display = idx > 0 ? '' : 'none';
// Bei echtem Step-Wechsel: Position animieren (suppressAnim = false)
suppressAnim = false;
layoutCurrent();
// Nach kurzem Delay zurück in den Scroll-Follow-Modus (no anim)
setTimeout(() => { suppressAnim = true; }, 80);
// Text mit Typewriter starten (Figur-Token ersetzen)
const figEmoji = document.getElementById('tourFigure').textContent;
const text = step.text.replace(/\{fig\}/g, figEmoji);
startTypewriter(text);
}
function layoutCurrent() {
if (!active) return;
const step = TOUR_STEPS[stepIdx];
const root = document.getElementById('tourRoot');
const highlight = document.getElementById('tourHighlight');
const character = document.getElementById('tourCharacter');
if (!step) return;
// Transition-Klassen für scroll-follow vs. step-change
if (suppressAnim) {
character.classList.add('tour-no-anim');
highlight.classList.add('tour-no-anim');
} else {
character.classList.remove('tour-no-anim');
highlight.classList.remove('tour-no-anim');
}
let targetEl = null;
if (step.selector) {
const cands = document.querySelectorAll(step.selector);
for (const c of cands) {
if (c.offsetWidth > 0 && c.offsetHeight > 0) { targetEl = c; break; }
}
}
if (!targetEl) {
// Kein Anker → Hintergrund dunkel (no-anchor-Modus), Figur mittig, kein Highlight
root.classList.add('no-anchor');
highlight.style.display = 'none';
character.style.left = (window.innerWidth/2 - 200) + 'px';
character.style.top = (window.innerHeight/2 - 120) + 'px';
character.dataset.side = 'center';
return;
}
root.classList.remove('no-anchor');
// Element ins Viewport scrollen, falls außerhalb
const r = targetEl.getBoundingClientRect();
const PAD = 10;
const inView = r.top >= 0 && r.bottom <= window.innerHeight;
if (!inView) targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Highlight positionieren
highlight.style.display = '';
highlight.style.top = (r.top - PAD) + 'px';
highlight.style.left = (r.left - PAD) + 'px';
highlight.style.width = (r.width + PAD * 2) + 'px';
highlight.style.height = (r.height + PAD * 2) + 'px';
// Figur+Bubble platzieren — Seite je nach Anker
const charW = 380, charH = 200;
let side = step.side || 'right';
let cx, cy;
if (side === 'right') {
cx = r.right + 24;
cy = r.top + r.height/2 - charH/2;
if (cx + charW > window.innerWidth - 12) side = 'left';
}
if (side === 'left') {
cx = r.left - charW - 24;
cy = r.top + r.height/2 - charH/2;
if (cx < 12) side = 'bottom';
}
if (side === 'top') {
cx = r.left + r.width/2 - charW/2;
cy = r.top - charH - 16;
if (cy < 12) side = 'bottom';
}
if (side === 'bottom') {
cx = r.left + r.width/2 - charW/2;
cy = r.bottom + 16;
}
// Clamp ins Viewport
cx = Math.max(12, Math.min(window.innerWidth - charW - 12, cx));
cy = Math.max(12, Math.min(window.innerHeight - charH - 12, cy));
character.style.left = cx + 'px';
character.style.top = cy + 'px';
character.dataset.side = side;
}
// ── Typewriter ──
function startTypewriter(text) {
if (typingTimer) clearInterval(typingTimer);
typingFull = text;
typingPos = 0;
const el = document.getElementById('tourBubbleText');
el.textContent = '';
typingTimer = setInterval(() => {
typingPos++;
el.textContent = typingFull.slice(0, typingPos);
if (typingPos >= typingFull.length) {
clearInterval(typingTimer);
typingTimer = null;
}
}, 38);
}
function skipTypewriter() {
if (!typingTimer) return;
clearInterval(typingTimer);
typingTimer = null;
document.getElementById('tourBubbleText').textContent = typingFull;
}
// Klick auf den Text-Bereich → Tippen überspringen
document.addEventListener('click', (e) => {
if (!active) return;
const text = document.getElementById('tourBubbleText');
if (text && text.contains(e.target) && typingTimer) {
e.stopPropagation();
skipTypewriter();
}
});
// ── Auto-Trigger beim ersten Aufruf von Schritt 3 ──
window.maybeStartTour = function () {
try {
if (localStorage.getItem('tourSchritt3Done') === '1') return;
} catch (e) {}
// Tour erst nach kurzem Delay starten, damit Layout/Render fertig sind
setTimeout(() => tourStart(), 350);
};
})();