AP4 Spielanleitungen & Hints: gemeinsames Guide-Modul + MindMatch Paar-Blitz

- game-guide.js/.css (neu): gemeinsames "So funktioniert's"-Overlay mit ?-Button
  in der Header-Tools-Leiste, Auto-Open beim Erstbesuch (localStorage
  mf-guide-seen:<slug>), Escape/Backdrop schliesst, Fokus-Trap; deutsches Inline-
  Registry fuer 7 Spiele (Networker bleibt bewusst bei seinem Inline-Guide). API
  window.MindfluxGuide = {open, close, registerHintProvider} fuer einheitliche
  Tipp-Buttons. Injektion via theme.js nur auf Game-Routen (wie FluxTimer), keine
  Template-Edits.
- mindmatch.js: Hint "Paar-Blitz" (3 Ladungen) deckt ~1s ein ungematchtes Paar
  auf und kostet +1 Zug -> Score/Sterne degradieren ueber die bestehende
  Serverformel (null Backend-Aenderung); Reset in newGame, Verstecken in
  finish/cancelGame; .mm-card.hinted-Glow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFxcjtc9HNP4VECT6Ur1tE
This commit is contained in:
AliceDarkest
2026-07-21 00:32:11 +02:00
parent f8f068150e
commit 983e57cd8d
4 changed files with 561 additions and 0 deletions
+95
View File
@@ -34,6 +34,10 @@
let timerId = null;
let running = false; // läuft gerade eine Runde? (steuert den Abbrechen-Button)
// Tipp „Paar-Blitz": deckt kurz ein noch nicht gematchtes Paar auf (kostet +1 Zug).
let hintsLeft = 3;
let hintUi = null; // Handle des Tipp-Buttons (von MindfluxGuide oder eigen)
/* Punkteformel – identisch zu GameScoreService.calculateScore (Server bleibt autoritativ). */
function calculateScore(p, mv, secs) {
if (p <= 0) return 0;
@@ -175,6 +179,7 @@
stopTimer();
running = false;
cancelBtn.hidden = true;
hideHint();
const localScore = calculateScore(totalPairs, moves, seconds);
const starCount = starsFor(totalPairs, moves);
@@ -224,9 +229,95 @@
lock = false;
resultEl.hidden = true;
cancelBtn.hidden = true;
hideHint();
setIdle('Spiel abgebrochen – wähle ein Deck und starte ein neues Spiel.');
}
/* ---------- Tipp „Paar-Blitz" ----------
Deckt EIN noch nicht gematchtes Paar ~1 s auf und kostet +1 Zug, damit Score/Sterne
über die bestehende (Server-)Formel degradieren. Keine Backend-Änderung nötig. */
function revealHintPair() {
if (hintsLeft <= 0 || !running || lock) return hintsLeft;
const cards = Array.from(board.querySelectorAll('.mm-card:not(.matched)'));
if (!cards.length) return hintsLeft;
let a = null;
if (firstCard && !firstCard.classList.contains('matched')) {
a = firstCard; // schon eine Karte offen -> dessen Partner zeigen
} else {
const closed = cards.filter((c) => !c.classList.contains('flipped'));
const pool = closed.length ? closed : cards;
a = pool[Math.floor(Math.random() * pool.length)];
}
const b = cards.find((c) => c !== a && c.dataset.pairId === a.dataset.pairId);
if (!a || !b) return hintsLeft;
hintsLeft--;
// +1 Zug: der Server rechnet Score/Sterne allein aus moves.
moves++;
movesEl.textContent = String(moves);
[a, b].forEach((c) => c.classList.add('flipped', 'hinted'));
lock = true; // kurz sperren, damit niemand ins aufgedeckte Paar klickt
setTimeout(() => {
[a, b].forEach((c) => {
c.classList.remove('hinted');
if (c === firstCard || c.classList.contains('matched')) return;
c.classList.remove('flipped');
});
lock = false;
}, 1000);
return hintsLeft;
}
function refreshHint() { if (hintUi && hintUi.update) hintUi.update(hintsLeft); }
function showHint() { if (hintUi) { if (hintUi.show) hintUi.show(); refreshHint(); } }
function hideHint() { if (hintUi && hintUi.hide) hintUi.hide(); }
/* Fallback-Button in der .mm-bar-group, falls MindfluxGuide nicht verfügbar ist. */
function buildOwnHintButton() {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'mm-btn mm-btn-hint';
function paint() {
btn.textContent = `Tipp (${hintsLeft})`;
btn.disabled = hintsLeft <= 0 || !running;
}
btn.addEventListener('click', () => { revealHintPair(); paint(); });
const group = document.querySelector('.mm-bar-group');
if (group) group.appendChild(btn);
paint();
return {
update: paint,
show() { btn.hidden = false; paint(); },
hide() { btn.hidden = true; }
};
}
/* Bevorzugt den einheitlichen Tipp-Button von MindfluxGuide; wartet kurz auf dessen
API (Ladereihenfolge nicht garantiert) und fällt sonst auf einen eigenen Button zurück. */
function setupHint(tries) {
if (hintUi) return;
const api = window.MindfluxGuide;
if (api && typeof api.registerHintProvider === 'function') {
hintUi = api.registerHintProvider({
label: 'Tipp',
remaining: () => hintsLeft,
onHint: revealHintPair
});
} else if ((tries || 0) < 25) {
setTimeout(() => setupHint((tries || 0) + 1), 60);
return;
} else {
hintUi = buildOwnHintButton();
}
if (running) showHint(); else hideHint();
}
async function loadStats() {
try {
const res = await fetch(`/api/games/${GAME}/stats`);
@@ -249,8 +340,10 @@
resultEl.hidden = true;
firstCard = null;
lock = false;
hintsLeft = 3;
if (!deckPairs || !deckPairs.length) {
hideHint();
setIdle('Dieses Deck hat noch keine Paare.');
return;
}
@@ -262,6 +355,7 @@
running = true;
cancelBtn.hidden = false;
showHint();
}
async function loadDecks() {
@@ -292,6 +386,7 @@
againBtn.addEventListener('click', newGame);
cancelBtn.addEventListener('click', cancelGame);
deckSelect.addEventListener('change', newGame);
setupHint();
await loadStats();