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 timerId = null;
|
||||||
let running = false; // läuft gerade eine Runde? (steuert den Abbrechen-Button)
|
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). */
|
/* Punkteformel – identisch zu GameScoreService.calculateScore (Server bleibt autoritativ). */
|
||||||
function calculateScore(p, mv, secs) {
|
function calculateScore(p, mv, secs) {
|
||||||
if (p <= 0) return 0;
|
if (p <= 0) return 0;
|
||||||
@@ -175,6 +179,7 @@
|
|||||||
stopTimer();
|
stopTimer();
|
||||||
running = false;
|
running = false;
|
||||||
cancelBtn.hidden = true;
|
cancelBtn.hidden = true;
|
||||||
|
hideHint();
|
||||||
|
|
||||||
const localScore = calculateScore(totalPairs, moves, seconds);
|
const localScore = calculateScore(totalPairs, moves, seconds);
|
||||||
const starCount = starsFor(totalPairs, moves);
|
const starCount = starsFor(totalPairs, moves);
|
||||||
@@ -224,9 +229,95 @@
|
|||||||
lock = false;
|
lock = false;
|
||||||
resultEl.hidden = true;
|
resultEl.hidden = true;
|
||||||
cancelBtn.hidden = true;
|
cancelBtn.hidden = true;
|
||||||
|
hideHint();
|
||||||
setIdle('Spiel abgebrochen – wähle ein Deck und starte ein neues Spiel.');
|
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() {
|
async function loadStats() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/games/${GAME}/stats`);
|
const res = await fetch(`/api/games/${GAME}/stats`);
|
||||||
@@ -249,8 +340,10 @@
|
|||||||
resultEl.hidden = true;
|
resultEl.hidden = true;
|
||||||
firstCard = null;
|
firstCard = null;
|
||||||
lock = false;
|
lock = false;
|
||||||
|
hintsLeft = 3;
|
||||||
|
|
||||||
if (!deckPairs || !deckPairs.length) {
|
if (!deckPairs || !deckPairs.length) {
|
||||||
|
hideHint();
|
||||||
setIdle('Dieses Deck hat noch keine Paare.');
|
setIdle('Dieses Deck hat noch keine Paare.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -262,6 +355,7 @@
|
|||||||
|
|
||||||
running = true;
|
running = true;
|
||||||
cancelBtn.hidden = false;
|
cancelBtn.hidden = false;
|
||||||
|
showHint();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDecks() {
|
async function loadDecks() {
|
||||||
@@ -292,6 +386,7 @@
|
|||||||
againBtn.addEventListener('click', newGame);
|
againBtn.addEventListener('click', newGame);
|
||||||
cancelBtn.addEventListener('click', cancelGame);
|
cancelBtn.addEventListener('click', cancelGame);
|
||||||
deckSelect.addEventListener('change', newGame);
|
deckSelect.addEventListener('change', newGame);
|
||||||
|
setupHint();
|
||||||
|
|
||||||
await loadStats();
|
await loadStats();
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,25 @@
|
|||||||
document.head.appendChild(accessibilityScript);
|
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 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>';
|
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