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
@@ -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; }
}
+280
View File
@@ -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 };
})();
+95
View File
@@ -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();
+19
View File
@@ -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>';