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:
@@ -0,0 +1,167 @@
|
||||
/* Mindflux – Gemeinsames Guide-Modul „So funktioniert's".
|
||||
Styles für den „?"-Trigger, den Tipp-Button, das Overlay und den MindMatch-Hint-Glow.
|
||||
Theme-bewusst: dunkel als Standard, helle Überschreibungen für data-theme="light". */
|
||||
|
||||
/* ---------- Buttons in der Header-Tools-Leiste ---------- */
|
||||
.mf-guide-trigger,
|
||||
.mf-guide-hint {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 999px;
|
||||
transition: background-color .2s ease, border-color .2s ease, transform .12s ease;
|
||||
}
|
||||
.mf-guide-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.1rem;
|
||||
height: 2.1rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
.mf-guide-hint {
|
||||
padding: .34rem .8rem;
|
||||
font-size: .85rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mf-guide-trigger:hover,
|
||||
.mf-guide-hint:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.mf-guide-trigger:focus-visible,
|
||||
.mf-guide-hint:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.mf-guide-hint:disabled {
|
||||
opacity: .45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ---------- Overlay ---------- */
|
||||
.mf-guide-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.25rem;
|
||||
background: rgba(5, 8, 16, 0.72);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.mf-guide-backdrop[hidden] { display: none; }
|
||||
|
||||
.mf-guide-panel {
|
||||
width: min(34rem, 100%);
|
||||
max-height: min(82vh, 640px);
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
padding: 1.4rem 1.5rem 1.6rem;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
background: #141a26;
|
||||
color: #eef2f8;
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
|
||||
animation: mf-guide-in .22s ease;
|
||||
}
|
||||
@keyframes mf-guide-in {
|
||||
from { opacity: 0; transform: translateY(12px) scale(.98); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
.mf-guide-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: .4rem;
|
||||
}
|
||||
.mf-guide-kicker {
|
||||
font-size: .72rem;
|
||||
letter-spacing: .16em;
|
||||
text-transform: uppercase;
|
||||
opacity: .7;
|
||||
}
|
||||
.mf-guide-close {
|
||||
flex: none;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1;
|
||||
color: inherit;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.mf-guide-close:hover { background: rgba(255, 255, 255, 0.18); }
|
||||
.mf-guide-title {
|
||||
margin: 0 0 .9rem;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.mf-guide-steps {
|
||||
margin: 0 0 1rem;
|
||||
padding-left: 1.2rem;
|
||||
display: grid;
|
||||
gap: .5rem;
|
||||
}
|
||||
.mf-guide-steps li { line-height: 1.45; }
|
||||
.mf-guide-tip {
|
||||
margin: 0;
|
||||
padding: .75rem .9rem;
|
||||
border-radius: 12px;
|
||||
line-height: 1.45;
|
||||
background: rgba(120, 170, 255, 0.14);
|
||||
border: 1px solid rgba(120, 170, 255, 0.3);
|
||||
}
|
||||
body.mf-guide-open { overflow: hidden; }
|
||||
|
||||
/* ---------- Helles Theme ---------- */
|
||||
:root[data-theme="light"] .mf-guide-trigger,
|
||||
:root[data-theme="light"] .mf-guide-hint {
|
||||
border-color: rgba(20, 30, 50, 0.28);
|
||||
background: rgba(20, 30, 50, 0.05);
|
||||
}
|
||||
:root[data-theme="light"] .mf-guide-trigger:hover,
|
||||
:root[data-theme="light"] .mf-guide-hint:hover:not(:disabled) {
|
||||
background: rgba(20, 30, 50, 0.12);
|
||||
border-color: rgba(20, 30, 50, 0.45);
|
||||
}
|
||||
:root[data-theme="light"] .mf-guide-backdrop { background: rgba(15, 23, 42, 0.5); }
|
||||
:root[data-theme="light"] .mf-guide-panel {
|
||||
background: #ffffff;
|
||||
color: #16202e;
|
||||
border-color: rgba(20, 30, 50, 0.12);
|
||||
}
|
||||
:root[data-theme="light"] .mf-guide-close { background: rgba(20, 30, 50, 0.08); }
|
||||
:root[data-theme="light"] .mf-guide-close:hover { background: rgba(20, 30, 50, 0.16); }
|
||||
:root[data-theme="light"] .mf-guide-tip {
|
||||
background: rgba(60, 110, 220, 0.12);
|
||||
border-color: rgba(60, 110, 220, 0.28);
|
||||
}
|
||||
|
||||
/* ---------- MindMatch – kurz aufgedecktes Tipp-Paar ---------- */
|
||||
.mm-card.hinted {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 0 2px #ffd54a, 0 0 22px 5px rgba(255, 213, 74, 0.6);
|
||||
border-radius: 14px;
|
||||
animation: mf-hint-pulse 1s ease;
|
||||
}
|
||||
@keyframes mf-hint-pulse {
|
||||
0%, 100% { filter: none; }
|
||||
50% { filter: brightness(1.12); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mf-guide-panel { animation: none; }
|
||||
.mm-card.hinted { animation: none; }
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/* Mindflux – Gemeinsames Guide-Modul „So funktioniert's".
|
||||
- Erkennt das aktuelle Spiel über DOM-Marker (analog accessibility.js applySkin).
|
||||
- Hängt einen „?"-Button in die Header-Tools-Leiste des Spiels ein.
|
||||
- Öffnet ein Overlay mit einer kurzen Anleitung; der Erstbesuch öffnet automatisch.
|
||||
- Stellt window.MindfluxGuide = { open, close, registerHintProvider } bereit.
|
||||
|
||||
Kein fetch: alle Inhalte liegen als deutsches Inline-Registry vor (Vorbild: networker-guide.js).
|
||||
|
||||
API window.MindfluxGuide:
|
||||
open() – Anleitung anzeigen.
|
||||
close() – Anleitung schließen.
|
||||
registerHintProvider({ label, remaining, onHint }) – einheitlichen Tipp-Button
|
||||
in der Header-Tools-Leiste erzeugen.
|
||||
label : String – Beschriftung des Buttons (z.B. "Tipp").
|
||||
remaining : Function -> Number – liefert die aktuelle Restanzahl (für die Beschriftung).
|
||||
onHint : Function – wird beim Klick aufgerufen; darf die neue Restanzahl zurückgeben.
|
||||
Rückgabe: { update(n), show(), hide(), remove(), button } zum Steuern des Buttons. */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// NetWorker bringt seinen eigenen Inline-Guide mit -> hier bewusst nichts tun.
|
||||
if (document.querySelector('.nw-app')) return;
|
||||
|
||||
// ---------- Spiel-Erkennung über DOM-Marker ----------
|
||||
var MARKERS = [
|
||||
['.mm-body', 'mindmatch'],
|
||||
['.tt-body', 'techtacular'],
|
||||
['.ss-body', 'syntaxsaga'],
|
||||
['.esc-app', 'codeescape'],
|
||||
['.dlg-body', 'devlab'],
|
||||
['.bh-app', 'bughunter'],
|
||||
['.sc-app', 'serviceclash']
|
||||
];
|
||||
var slug = '';
|
||||
for (var i = 0; i < MARKERS.length; i++) {
|
||||
if (document.querySelector(MARKERS[i][0])) { slug = MARKERS[i][1]; break; }
|
||||
}
|
||||
if (!slug) return;
|
||||
|
||||
// ---------- Registry: je Spiel Titel + „So funktioniert's" (Schritte + 1 Tipp) ----------
|
||||
var GUIDES = {
|
||||
mindmatch: {
|
||||
title: 'MindMatch – So funktioniert’s',
|
||||
steps: [
|
||||
'Wähle oben ein Deck und klicke auf „Neues Spiel".',
|
||||
'Decke zwei Karten auf und finde einen Begriff und die passende Bedeutung.',
|
||||
'Ein richtiges Paar bleibt liegen, ein Fehlgriff dreht wieder zu.',
|
||||
'Finde alle Paare – mit möglichst wenigen Zügen.'
|
||||
],
|
||||
tip: 'Je weniger Züge, desto mehr Sterne. Der „Tipp"-Button deckt kurz ein Paar auf, kostet dich dafür aber einen Zug.'
|
||||
},
|
||||
techtacular: {
|
||||
title: 'TechTacular – So funktioniert’s',
|
||||
steps: [
|
||||
'Starte mit „Neues Spiel" – das Jeopardy-Board mit Kategorien erscheint.',
|
||||
'Wähle eine Kachel: mehr Punkte bedeuten eine kniffligere Aufgabe.',
|
||||
'Lies die Antwort und wähle aus den Optionen die dazu passende Frage.',
|
||||
'Sammle über alle Fragen hinweg möglichst viele Punkte.'
|
||||
],
|
||||
tip: 'Der 50:50-Joker entfernt zwei falsche Optionen – dafür gibt es bei dieser Frage nur die halben Punkte.'
|
||||
},
|
||||
syntaxsaga: {
|
||||
title: 'SyntaxSaga – So funktioniert’s',
|
||||
steps: [
|
||||
'Wähle oben eine Stufe: Junior, Professional oder Senior.',
|
||||
'Lies die Geschichte und fülle die Code-Lücken in den Eingabefeldern.',
|
||||
'Klicke „Prüfen" (oder drücke Enter) und arbeite dich durch die Missionen.'
|
||||
],
|
||||
tip: 'Der erste richtige Versuch bringt volle Punkte – nutze einen Hinweis, wenn du feststeckst.'
|
||||
},
|
||||
codeescape: {
|
||||
title: 'SystemEscape – So funktioniert’s',
|
||||
steps: [
|
||||
'Wähle ein Szenario und generiere einen Run mit vier Räumen.',
|
||||
'Löse in jedem Raum die Rätsel und sichere so die Fragmente.',
|
||||
'Setze die Fragmente von links nach rechts zum Türcode zusammen und öffne die Schleuse.'
|
||||
],
|
||||
tip: 'Die Uhr läuft: „Hinweis" kostet nichts, „Auflösen" bringt dich schneller weiter – dafür ohne den vollen Ruhm.'
|
||||
},
|
||||
devlab: {
|
||||
title: 'Dev Lab – So funktioniert’s',
|
||||
steps: [
|
||||
'Lies die Aufgabe und den Hinweistext oben in der Karte.',
|
||||
'Löse sie im Spielfeld – je nach Typ: Lücken füllen, auswählen, zuordnen, Bug finden oder Regex.',
|
||||
'Klicke „Prüfen" und danach „Weiter" zur nächsten Aufgabe.'
|
||||
],
|
||||
tip: 'Der erste richtige Versuch zählt voll. „Hinweis" und „Auflösung" helfen, wenn es klemmt.'
|
||||
},
|
||||
bughunter: {
|
||||
title: 'BugHunter – So funktioniert’s',
|
||||
steps: [
|
||||
'Öffne eine Fallakte und lies Briefing sowie Konsolenausgabe.',
|
||||
'Orte die fehlerhafte Zeile und klassifiziere den Fehler (Syntax, Logik, Laufzeit, Sicherheit).',
|
||||
'Wähle den sauberen Fix und bestätige mit „Prüfen".'
|
||||
],
|
||||
tip: 'Löst du eine Akte im ersten Versuch fehlerfrei, baust du eine Combo-Serie mit Bonuspunkten auf.'
|
||||
},
|
||||
serviceclash: {
|
||||
title: 'ServiceClash – So funktioniert’s',
|
||||
steps: [
|
||||
'Wähle eine Schicht und öffne den Leitstand.',
|
||||
'Entscheide je Ticket zuerst: Störung oder Anfrage, dann die Priorität.',
|
||||
'Wähle die beste erste Maßnahme – das Ticket wird dokumentiert.'
|
||||
],
|
||||
tip: '„Antwort aufdecken" zeigt dir die Begründung, gibt für diesen Schritt aber keine Punkte.'
|
||||
}
|
||||
};
|
||||
|
||||
var guide = GUIDES[slug];
|
||||
if (!guide) return;
|
||||
|
||||
var SEEN_KEY = 'mf-guide-seen:' + slug;
|
||||
|
||||
// ---------- Header-Tools-Leiste finden (Container kann spät im DOM erscheinen) ----------
|
||||
var HOST_SEL = '.mm-header-tools,.tt-header-tools,.ss-header-tools,.dlg-header-tools,.mf-a11y-host';
|
||||
var pending = [];
|
||||
var hostObserver = null;
|
||||
|
||||
function flushPending() {
|
||||
var host = document.querySelector(HOST_SEL);
|
||||
if (!host) return false;
|
||||
while (pending.length) { pending.shift()(host); }
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ruft fn(host) auf, sobald die Leiste existiert – sofort oder per MutationObserver.
|
||||
function whenHost(fn) {
|
||||
pending.push(fn);
|
||||
if (flushPending()) return;
|
||||
if (!hostObserver) {
|
||||
hostObserver = new MutationObserver(function () {
|
||||
if (flushPending() && !pending.length && hostObserver) {
|
||||
hostObserver.disconnect();
|
||||
hostObserver = null;
|
||||
}
|
||||
});
|
||||
hostObserver.observe(document.documentElement, { childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Overlay (Backdrop + Panel) ----------
|
||||
var backdrop = null;
|
||||
var panel = null;
|
||||
var closeBtn = null;
|
||||
var trigger = null;
|
||||
var lastFocus = null;
|
||||
|
||||
function buildOverlay() {
|
||||
if (backdrop) return;
|
||||
backdrop = document.createElement('div');
|
||||
backdrop.className = 'mf-guide-backdrop';
|
||||
backdrop.hidden = true;
|
||||
|
||||
panel = document.createElement('section');
|
||||
panel.className = 'mf-guide-panel';
|
||||
panel.setAttribute('role', 'dialog');
|
||||
panel.setAttribute('aria-modal', 'true');
|
||||
panel.setAttribute('aria-labelledby', 'mf-guide-title');
|
||||
|
||||
var steps = guide.steps.map(function (s) { return '<li>' + s + '</li>'; }).join('');
|
||||
panel.innerHTML =
|
||||
'<div class="mf-guide-head">' +
|
||||
'<span class="mf-guide-kicker">So funktioniert’s</span>' +
|
||||
'<button type="button" class="mf-guide-close" aria-label="Anleitung schließen">×</button>' +
|
||||
'</div>' +
|
||||
'<h2 id="mf-guide-title" class="mf-guide-title">' + guide.title + '</h2>' +
|
||||
'<ol class="mf-guide-steps">' + steps + '</ol>' +
|
||||
'<p class="mf-guide-tip"><strong>Tipp:</strong> ' + guide.tip + '</p>';
|
||||
|
||||
backdrop.appendChild(panel);
|
||||
document.body.appendChild(backdrop);
|
||||
|
||||
closeBtn = panel.querySelector('.mf-guide-close');
|
||||
closeBtn.addEventListener('click', close);
|
||||
backdrop.addEventListener('mousedown', function (e) {
|
||||
if (e.target === backdrop) close();
|
||||
});
|
||||
}
|
||||
|
||||
function onKeydown(e) {
|
||||
if (e.key === 'Escape') { e.stopPropagation(); close(); return; }
|
||||
if (e.key !== 'Tab') return;
|
||||
var f = panel.querySelectorAll('button, [href], input, [tabindex]:not([tabindex="-1"])');
|
||||
if (!f.length) return;
|
||||
var first = f[0], last = f[f.length - 1];
|
||||
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
||||
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
||||
}
|
||||
|
||||
function open() {
|
||||
buildOverlay();
|
||||
if (!backdrop.hidden) return;
|
||||
lastFocus = document.activeElement;
|
||||
backdrop.hidden = false;
|
||||
document.body.classList.add('mf-guide-open');
|
||||
document.addEventListener('keydown', onKeydown, true);
|
||||
if (closeBtn) closeBtn.focus();
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!backdrop || backdrop.hidden) return;
|
||||
backdrop.hidden = true;
|
||||
document.body.classList.remove('mf-guide-open');
|
||||
document.removeEventListener('keydown', onKeydown, true);
|
||||
var back = trigger || lastFocus;
|
||||
if (back && typeof back.focus === 'function') back.focus();
|
||||
}
|
||||
|
||||
// ---------- „?"-Trigger in die Header-Tools-Leiste ----------
|
||||
function mountTrigger(host) {
|
||||
if (trigger || host.querySelector('.mf-guide-trigger')) return;
|
||||
trigger = document.createElement('button');
|
||||
trigger.type = 'button';
|
||||
trigger.className = 'mf-guide-trigger';
|
||||
trigger.setAttribute('aria-label', 'Spielanleitung öffnen');
|
||||
trigger.setAttribute('aria-haspopup', 'dialog');
|
||||
trigger.innerHTML = '<span aria-hidden="true">?</span>';
|
||||
trigger.addEventListener('click', open);
|
||||
host.insertBefore(trigger, host.firstChild);
|
||||
}
|
||||
|
||||
// ---------- Einheitlicher Tipp-Button ----------
|
||||
function registerHintProvider(opts) {
|
||||
opts = opts || {};
|
||||
var label = opts.label || 'Tipp';
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'mf-guide-hint';
|
||||
|
||||
function readRemaining(explicit) {
|
||||
if (typeof explicit === 'number') return explicit;
|
||||
if (typeof opts.remaining === 'function') {
|
||||
var v = opts.remaining();
|
||||
return typeof v === 'number' ? v : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function paint(explicit) {
|
||||
var rest = readRemaining(explicit);
|
||||
btn.textContent = rest === null ? label : label + ' (' + rest + ')';
|
||||
btn.disabled = rest !== null && rest <= 0;
|
||||
btn.setAttribute('aria-label', label + (rest === null ? '' : ' – noch ' + rest + ' übrig'));
|
||||
}
|
||||
btn.addEventListener('click', function () {
|
||||
if (btn.disabled) return;
|
||||
var ret = typeof opts.onHint === 'function' ? opts.onHint() : null;
|
||||
paint(typeof ret === 'number' ? ret : undefined);
|
||||
});
|
||||
paint();
|
||||
whenHost(function (host) { host.insertBefore(btn, host.firstChild); });
|
||||
|
||||
return {
|
||||
update: function (n) { paint(n); },
|
||||
show: function () { btn.hidden = false; paint(); },
|
||||
hide: function () { btn.hidden = true; },
|
||||
remove: function () { if (btn.parentNode) btn.parentNode.removeChild(btn); },
|
||||
button: btn
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- Start ----------
|
||||
whenHost(mountTrigger);
|
||||
|
||||
// Erstbesuch: Anleitung automatisch öffnen, danach nur noch per „?"-Button.
|
||||
var seen = false;
|
||||
try { seen = !!localStorage.getItem(SEEN_KEY); } catch (e) { seen = false; }
|
||||
if (!seen) {
|
||||
var openOnce = function () {
|
||||
open();
|
||||
try { localStorage.setItem(SEEN_KEY, '1'); } catch (e) {}
|
||||
};
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', openOnce);
|
||||
} else {
|
||||
openOnce();
|
||||
}
|
||||
}
|
||||
|
||||
window.MindfluxGuide = { open: open, close: close, registerHintProvider: registerHintProvider };
|
||||
})();
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -52,6 +52,25 @@
|
||||
document.head.appendChild(accessibilityScript);
|
||||
}
|
||||
|
||||
// Spielanleitungen & Hints („So funktioniert's" + einheitliche Tipp-Buttons):
|
||||
// nur auf den eigenständigen Spielrouten laden, analog zur FluxTimer-Injektion.
|
||||
if (isGameRoute) {
|
||||
if (!document.querySelector('link[data-game-guide]')) {
|
||||
var guideCss = document.createElement('link');
|
||||
guideCss.rel = 'stylesheet';
|
||||
guideCss.href = '/css/game-guide.css?v=1';
|
||||
guideCss.setAttribute('data-game-guide', '');
|
||||
document.head.appendChild(guideCss);
|
||||
}
|
||||
if (!document.querySelector('script[data-game-guide]')) {
|
||||
var guideScript = document.createElement('script');
|
||||
guideScript.src = '/js/game-guide.js?v=1';
|
||||
guideScript.defer = true;
|
||||
guideScript.setAttribute('data-game-guide', '');
|
||||
document.head.appendChild(guideScript);
|
||||
}
|
||||
}
|
||||
|
||||
var SUN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>';
|
||||
var MOON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user