Fix mobile broom flight with D-pad & touch-action, add Chamber II McGonagalls Wizard Chess

This commit is contained in:
Maxi
2026-09-22 19:24:26 +02:00
parent 9f5e5085ec
commit efb54befcc
10 changed files with 793 additions and 116 deletions
+154 -86
View File
@@ -1,4 +1,4 @@
// Broom flight mini-game: Winged Keys Chamber (Flitwick's Challenge)
// Broom flight mini-game: Winged Keys Chamber (Flitwick's Challenge) - Mobile & Touch Optimized
(function() {
const canvas = document.getElementById('broomCanvas');
if (!canvas) return;
@@ -13,15 +13,28 @@
const modalText = document.getElementById('modalText');
const modalBtn = document.getElementById('modalBtn');
let width, height;
let width = 800, height = 480;
function resize() {
width = canvas.parentElement.clientWidth;
height = Math.min(520, window.innerHeight * 0.65);
const rect = canvas.getBoundingClientRect();
width = Math.round(rect.width || canvas.parentElement.clientWidth || 800);
height = Math.round(Math.min(520, Math.max(300, width * 0.58)));
canvas.width = width;
canvas.height = height;
canvas.style.height = height + 'px';
if (door) {
door.x = width - 55;
door.y = height / 2 - 45;
}
}
window.addEventListener('resize', resize);
resize();
// Target Door
const door = {
x: width - 55,
y: height / 2 - 45,
w: 42,
h: 90
};
// Game State
let gameRunning = true;
@@ -33,10 +46,10 @@
// Player (Broomstick)
const player = {
x: 80,
y: height / 2,
y: 200,
vx: 0,
vy: 0,
speed: 5.5,
speed: 6.0,
boost: 1.0,
angle: 0,
trail: []
@@ -44,26 +57,22 @@
// Keys Swarm
const keys = [];
const NUM_KEYS = 24;
const NUM_KEYS = 22;
// Master Key (Damaged wing silver key)
const masterKey = {
x: width - 150,
y: height / 2,
vx: -2,
x: 600,
y: 200,
vx: -2.5,
vy: 1.5,
wingPhase: 0,
caught: false,
size: 16
size: 17
};
// Target Door
const door = {
x: width - 60,
y: height / 2 - 45,
w: 45,
h: 90
};
// Initialize Canvas Size
window.addEventListener('resize', resize);
resize();
// Spawn decoy keys
for (let i = 0; i < NUM_KEYS; i++) {
@@ -79,53 +88,106 @@
});
}
// Controls
// Keyboard Controls
const keysDown = {};
window.addEventListener('keydown', e => {
keysDown[e.key.toLowerCase()] = true;
if (e.key === ' ' || e.key === 'Shift') player.boost = 1.6;
if (e.key === ' ' || e.key === 'Shift') {
player.boost = 1.7;
if (window.soundFX) window.soundFX.playWhoosh();
}
});
window.addEventListener('keyup', e => {
keysDown[e.key.toLowerCase()] = false;
if (e.key === ' ' || e.key === 'Shift') player.boost = 1.0;
});
// Pointer / Touch control
// Touch & Pointer Steering
let pointerTarget = null;
canvas.addEventListener('pointermove', e => {
function getCanvasCoords(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
pointerTarget = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
return {
x: (clientX - rect.left) * (canvas.width / rect.width),
y: (clientY - rect.top) * (canvas.height / rect.height)
};
});
}
// Direct Touch Listeners to prevent browser scrolling
canvas.addEventListener('touchstart', e => {
e.preventDefault();
if (e.touches.length > 0) {
pointerTarget = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY);
player.boost = 1.4;
if (window.soundFX) window.soundFX.playWhoosh();
}
}, { passive: false });
canvas.addEventListener('touchmove', e => {
e.preventDefault();
if (e.touches.length > 0) {
pointerTarget = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY);
}
}, { passive: false });
canvas.addEventListener('touchend', e => {
e.preventDefault();
pointerTarget = null;
player.boost = 1.0;
}, { passive: false });
// Pointer events for mouse
canvas.addEventListener('pointerdown', e => {
player.boost = 1.6;
if (window.soundFX) window.soundFX.playWhoosh();
if (e.pointerType === 'mouse') {
pointerTarget = getCanvasCoords(e.clientX, e.clientY);
player.boost = 1.5;
if (window.soundFX) window.soundFX.playWhoosh();
}
});
canvas.addEventListener('pointermove', e => {
if (e.pointerType === 'mouse' && (e.buttons > 0 || pointerTarget)) {
pointerTarget = getCanvasCoords(e.clientX, e.clientY);
}
});
canvas.addEventListener('pointerup', () => {
player.boost = 1.0;
});
canvas.addEventListener('pointerleave', () => {
pointerTarget = null;
player.boost = 1.0;
});
// Mobile buttons
const btnUp = document.getElementById('mBtnUp');
const btnDown = document.getElementById('mBtnDown');
// Mobile D-Pad Buttons
function bindDpad(btnId, keyName) {
const btn = document.getElementById(btnId);
if (!btn) return;
const start = (e) => { e.preventDefault(); keysDown[keyName] = true; };
const end = (e) => { e.preventDefault(); keysDown[keyName] = false; };
btn.addEventListener('touchstart', start, { passive: false });
btn.addEventListener('touchend', end, { passive: false });
btn.addEventListener('pointerdown', start);
btn.addEventListener('pointerup', end);
btn.addEventListener('pointerleave', end);
}
bindDpad('mBtnUp', 'arrowup');
bindDpad('mBtnDown', 'arrowdown');
bindDpad('mBtnLeft', 'arrowleft');
bindDpad('mBtnRight', 'arrowright');
const btnBoost = document.getElementById('mBtnBoost');
if (btnUp) {
btnUp.addEventListener('pointerdown', () => { keysDown['arrowup'] = true; });
btnUp.addEventListener('pointerup', () => { keysDown['arrowup'] = false; });
}
if (btnDown) {
btnDown.addEventListener('pointerdown', () => { keysDown['arrowdown'] = true; });
btnDown.addEventListener('pointerup', () => { keysDown['arrowdown'] = false; });
}
if (btnBoost) {
btnBoost.addEventListener('pointerdown', () => { player.boost = 1.8; if (window.soundFX) window.soundFX.playWhoosh(); });
btnBoost.addEventListener('pointerup', () => { player.boost = 1.0; });
const startBoost = (e) => {
e.preventDefault();
player.boost = 1.8;
if (window.soundFX) window.soundFX.playWhoosh();
};
const endBoost = (e) => {
e.preventDefault();
player.boost = 1.0;
};
btnBoost.addEventListener('touchstart', startBoost, { passive: false });
btnBoost.addEventListener('touchend', endBoost, { passive: false });
btnBoost.addEventListener('pointerdown', startBoost);
btnBoost.addEventListener('pointerup', endBoost);
btnBoost.addEventListener('pointerleave', endBoost);
}
function update() {
@@ -134,74 +196,73 @@
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
if (timerVal) timerVal.innerText = elapsedSec + 's';
// Keyboard Movement
// Keyboard & D-Pad Movement
let ax = 0, ay = 0;
if (keysDown['arrowup'] || keysDown['w']) ay -= 1;
if (keysDown['arrowdown'] || keysDown['s']) ay += 1;
if (keysDown['arrowleft'] || keysDown['a']) ax -= 1;
if (keysDown['arrowright'] || keysDown['d']) ax += 1;
// Pointer steer
// Pointer steer (Touch / Mouse direct following)
if (pointerTarget) {
const dx = pointerTarget.x - player.x;
const dy = pointerTarget.y - player.y;
const dist = Math.hypot(dx, dy);
if (dist > 15) {
if (dist > 12) {
ax = dx / dist;
ay = dy / dist;
}
}
const curSpeed = player.speed * player.boost;
player.vx = player.vx * 0.85 + ax * curSpeed * 0.2;
player.vy = player.vy * 0.85 + ay * curSpeed * 0.2;
player.vx = player.vx * 0.82 + ax * curSpeed * 0.22;
player.vy = player.vy * 0.82 + ay * curSpeed * 0.22;
player.x += player.vx;
player.y += player.vy;
// Bounds
player.x = Math.max(30, Math.min(width - 70, player.x));
player.x = Math.max(30, Math.min(width - 65, player.x));
player.y = Math.max(30, Math.min(height - 30, player.y));
player.angle = Math.atan2(player.vy, player.vx + 2) * 0.5;
player.angle = Math.atan2(player.vy, player.vx + 2) * 0.55;
// Particles trail
player.trail.push({ x: player.x - 20, y: player.y, alpha: 1, size: Math.random() * 4 + 2 });
if (player.trail.length > 20) player.trail.shift();
player.trail.forEach(p => { p.alpha *= 0.9; p.x -= 2; });
player.trail.push({ x: player.x - 22, y: player.y, alpha: 1, size: Math.random() * 4 + 2 });
if (player.trail.length > 22) player.trail.shift();
player.trail.forEach(p => { p.alpha *= 0.88; p.x -= 2; });
// Update decoy keys
keys.forEach(k => {
k.wingPhase += k.wingSpeed;
k.x += k.vx;
k.y += k.vy;
if (k.x < 50 || k.x > width - 100) k.vx *= -1;
if (k.x < 40 || k.x > width - 90) k.vx *= -1;
if (k.y < 30 || k.y > height - 30) k.vy *= -1;
});
// Update Master Key
if (!masterKey.caught) {
masterKey.wingPhase += 0.35;
// Evasive maneuver if player is close
const dToPlayer = Math.hypot(player.x - masterKey.x, player.y - masterKey.y);
if (dToPlayer < 140) {
masterKey.vx += (masterKey.x - player.x) * 0.04;
masterKey.vy += (masterKey.y - player.y) * 0.04;
if (dToPlayer < 150) {
masterKey.vx += (masterKey.x - player.x) * 0.045;
masterKey.vy += (masterKey.y - player.y) * 0.045;
} else {
masterKey.vx += (Math.random() - 0.5) * 0.8;
masterKey.vy += (Math.random() - 0.5) * 0.8;
masterKey.vx += (Math.random() - 0.5) * 0.9;
masterKey.vy += (Math.random() - 0.5) * 0.9;
}
masterKey.vx = Math.max(-5, Math.min(5, masterKey.vx * 0.95));
masterKey.vy = Math.max(-4, Math.min(4, masterKey.vy * 0.95));
masterKey.vx = Math.max(-5.5, Math.min(5.5, masterKey.vx * 0.95));
masterKey.vy = Math.max(-4.5, Math.min(4.5, masterKey.vy * 0.95));
masterKey.x += masterKey.vx;
masterKey.y += masterKey.vy;
masterKey.x = Math.max(120, Math.min(width - 120, masterKey.x));
masterKey.x = Math.max(90, Math.min(width - 100, masterKey.x));
masterKey.y = Math.max(40, Math.min(height - 40, masterKey.y));
// Catch check
if (dToPlayer < 35) {
// Catch check (generous hit box for mobile thumbs: 42px)
if (dToPlayer < 42) {
masterKey.caught = true;
keyCaught = true;
score += 500;
@@ -211,11 +272,10 @@
window.notifyNewItem({ icon: "🗝️", name: "Geknickter Silberschlüssel" });
}
if (statusMsg) {
statusMsg.innerHTML = "✨ <strong>Schlüssel gefangen & im Beutel!</strong> Fliege zur Holztür rechts und öffne das Schloss!";
statusMsg.innerHTML = "✨ <strong>Schlüssel gefangen & im Beutel!</strong> Fliege jetzt zur schweren Holztür rechts!";
statusMsg.style.color = "#2ecc71";
}
}
} else {
// Master key follows player
masterKey.x = player.x + 10;
@@ -223,7 +283,7 @@
// Check if player reached the door
const dToDoor = Math.hypot(player.x - (door.x + door.w / 2), player.y - (door.y + door.h / 2));
if (dToDoor < 55 && !doorUnlocked) {
if (dToDoor < 60 && !doorUnlocked) {
doorUnlocked = true;
gameRunning = false;
onGameWin(elapsedSec);
@@ -240,20 +300,20 @@
// Stone pillars
ctx.fillStyle = 'rgba(25, 22, 36, 0.7)';
for (let x = 120; x < width - 100; x += 180) {
ctx.fillRect(x, 0, 35, height);
for (let x = 110; x < width - 90; x += 160) {
ctx.fillRect(x, 0, 30, height);
ctx.strokeStyle = '#352e47';
ctx.lineWidth = 2;
ctx.strokeRect(x, 0, 35, height);
ctx.strokeRect(x, 0, 30, height);
}
// Vaulted arches at top
ctx.strokeStyle = '#2d273d';
ctx.lineWidth = 4;
ctx.lineWidth = 3;
ctx.beginPath();
for (let x = 0; x < width; x += 90) {
for (let x = 0; x < width; x += 80) {
ctx.moveTo(x, 0);
ctx.quadraticCurveTo(x + 45, 60, x + 90, 0);
ctx.quadraticCurveTo(x + 40, 50, x + 80, 0);
}
ctx.stroke();
@@ -285,11 +345,19 @@
});
ctx.shadowBlur = 0;
// Touch target indicator
if (pointerTarget && gameRunning) {
ctx.strokeStyle = 'rgba(229, 185, 72, 0.5)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(pointerTarget.x, pointerTarget.y, 20 + Math.sin(Date.now() * 0.01) * 4, 0, Math.PI * 2);
ctx.stroke();
}
// Decoy keys
keys.forEach(k => {
ctx.save();
ctx.translate(k.x, k.y);
// wings
const wingY = Math.sin(k.wingPhase) * (k.size * 0.7);
ctx.fillStyle = k.gold ? 'rgba(255, 215, 0, 0.7)' : 'rgba(200, 220, 255, 0.65)';
ctx.beginPath();
@@ -312,7 +380,7 @@
ctx.translate(masterKey.x, masterKey.y);
const mWingY = Math.sin(masterKey.wingPhase) * (masterKey.size * 0.8);
ctx.shadowColor = '#00ffff';
ctx.shadowBlur = masterKey.caught ? 5 : 12;
ctx.shadowBlur = masterKey.caught ? 6 : 14;
// Right intact wing
ctx.fillStyle = 'rgba(180, 225, 255, 0.85)';
@@ -320,12 +388,12 @@
ctx.ellipse(masterKey.size * 0.7, mWingY, masterKey.size * 0.9, masterKey.size * 0.4, -0.3, 0, Math.PI * 2);
ctx.fill();
// Left BENT wing (as in the book!)
// Left BENT wing
ctx.fillStyle = 'rgba(140, 190, 230, 0.75)';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(-masterKey.size * 0.5, mWingY * 0.6);
ctx.lineTo(-masterKey.size * 0.9, mWingY * 0.3); // bent angle!
ctx.lineTo(-masterKey.size * 0.9, mWingY * 0.3);
ctx.lineTo(-masterKey.size * 0.4, mWingY * 0.2);
ctx.closePath();
ctx.fill();
@@ -366,7 +434,7 @@
// Wizard silhouette (Cloak & Hat)
ctx.fillStyle = '#1c172a';
ctx.beginPath();
ctx.arc(5, -12, 7, 0, Math.PI * 2); // head
ctx.arc(5, -12, 7, 0, Math.PI * 2);
ctx.fill();
// Hat
ctx.fillStyle = '#120e1e';
@@ -377,7 +445,7 @@
ctx.closePath();
ctx.fill();
// Cloak fluttering
ctx.fillStyle = '#7a1f28'; // Gryffindor red cloak
ctx.fillStyle = '#7a1f28';
ctx.beginPath();
ctx.moveTo(0, -6);
ctx.lineTo(-20, -10 + Math.sin(Date.now() * 0.01) * 3);
@@ -421,9 +489,9 @@
modalText.innerHTML = `Mit waghalsigem Flug hast du den großen Silberschlüssel mit dem geknickten Flügel erwischt und das schwere Schloss entriegelt!<br><br>
<strong>Flugzeit:</strong> ${timeTaken} Sekunden<br>
<strong>Punkte:</strong> ${score}<br><br>
Die Tür schwingt knarrend auf... Vor dir lodern violette und schwarze Flammen in Snapes Prüfung!`;
modalBtn.innerText = "Weiter zur Flammenkammer 🧪";
modalBtn.onclick = () => { window.location.href = '/bottles'; };
Die Tür schwingt knarrend auf... Vor dir liegt McGonagalls gigantisches Zauberschach!`;
modalBtn.innerText = "Weiter zu McGonagalls Zauberschach ♟️";
modalBtn.onclick = () => { window.location.href = '/chess'; };
modalBackdrop.classList.add('show');
}
})();
+247
View File
@@ -0,0 +1,247 @@
// McGonagall's Wizard Chess (Zauberschach) Mini-Game
(function() {
const boardEl = document.getElementById('chessBoard');
if (!boardEl) return;
const statusMsg = document.getElementById('chessStatus');
const moveHistoryEl = document.getElementById('moveHistory');
const modalBackdrop = document.getElementById('modalBackdrop');
const modalTitle = document.getElementById('modalTitle');
const modalText = document.getElementById('modalText');
const modalBtn = document.getElementById('modalBtn');
const btnResetChess = document.getElementById('btnResetChess');
// Piece Unicode Symbols
const PIECES = {
'wK': '♔', 'wQ': '♕', 'wR': '♖', 'wB': '♗', 'wN': '♘', 'wP': '♙',
'bK': '♚', 'bQ': '♛', 'bR': '♜', 'bB': '♝', 'bN': '♞', 'bP': '♟'
};
// Initial Board State for "Rons Meisterzug" (Smothered Mate / Zauberschach-Taktik)
// Black to move: Knight on g4 and Queen on h4 attack White's castled King on h1!
let initialPosition = {
'h1': 'wK', 'g2': 'wP', 'h2': 'wP', 'f1': 'wR', 'd2': 'wQ', 'b2': 'wP', 'a1': 'wR',
'g8': 'bK', 'g7': 'bP', 'h7': 'bP', 'f8': 'bR', 'h4': 'bQ', 'g4': 'bN', 'c6': 'bP'
};
let boardState = {};
let selectedSquare = null;
let isSolved = false;
function initBoard() {
boardState = Object.assign({}, initialPosition);
selectedSquare = null;
isSolved = false;
if (statusMsg) {
statusMsg.innerHTML = "🧙‍♂️ <strong>Du führst Schwarz (Rons Springer & Hermines Figuren)!</strong> Finde den vernichtenden Zug gegen den weißen König!";
statusMsg.style.color = "var(--gold)";
}
if (moveHistoryEl) moveHistoryEl.innerHTML = "<li>Partie eröffnet: Schwarz am Zug!</li>";
renderBoard();
}
// Valid moves for Black pieces in this tactical puzzle
function getValidMoves(square) {
const piece = boardState[square];
if (!piece || !piece.startsWith('b')) return [];
const file = square.charCodeAt(0) - 97; // 0 to 7 (a-h)
const rank = parseInt(square[1], 10) - 1; // 0 to 7 (1-8)
const moves = [];
if (piece === 'bN') {
// Knight moves: L-shape
const offsets = [
[-2, -1], [-2, 1], [-1, -2], [-1, 2],
[1, -2], [1, 2], [2, -1], [2, 1]
];
offsets.forEach(([df, dr]) => {
const nf = file + df;
const nr = rank + dr;
if (nf >= 0 && nf < 8 && nr >= 0 && nr < 8) {
const targetSq = String.fromCharCode(97 + nf) + (nr + 1);
const targetPiece = boardState[targetSq];
if (!targetPiece || targetPiece.startsWith('w')) {
moves.push(targetSq);
}
}
});
} else if (piece === 'bQ') {
// Queen moves: orthogonal + diagonal
const directions = [
[0, 1], [0, -1], [1, 0], [-1, 0],
[1, 1], [1, -1], [-1, 1], [-1, -1]
];
directions.forEach(([df, dr]) => {
let step = 1;
while (true) {
const nf = file + df * step;
const nr = rank + dr * step;
if (nf < 0 || nf >= 8 || nr < 0 || nr >= 8) break;
const targetSq = String.fromCharCode(97 + nf) + (nr + 1);
const targetPiece = boardState[targetSq];
if (!targetPiece) {
moves.push(targetSq);
} else {
if (targetPiece.startsWith('w')) moves.push(targetSq);
break;
}
step++;
}
});
}
return moves;
}
function renderBoard() {
boardEl.innerHTML = '';
// 8 ranks (from 8 down to 1), 8 files (a to h)
for (let r = 8; r >= 1; r--) {
for (let f = 0; f < 8; f++) {
const sq = String.fromCharCode(97 + f) + r;
const isLight = (f + r) % 2 !== 0;
const tile = document.createElement('div');
tile.className = `chess-sq ${isLight ? 'light' : 'dark'}`;
tile.dataset.sq = sq;
if (selectedSquare === sq) {
tile.classList.add('selected');
}
const piece = boardState[sq];
if (piece) {
const pSpan = document.createElement('span');
pSpan.className = `chess-piece ${piece.startsWith('w') ? 'white-piece' : 'black-piece'}`;
pSpan.innerText = PIECES[piece] || '';
tile.appendChild(pSpan);
}
// Show valid moves
if (selectedSquare) {
const validMoves = getValidMoves(selectedSquare);
if (validMoves.includes(sq)) {
const dot = document.createElement('div');
dot.className = piece ? 'move-capture-ring' : 'move-dot';
tile.appendChild(dot);
}
}
tile.addEventListener('click', () => onSquareClick(sq));
boardEl.appendChild(tile);
}
}
}
function onSquareClick(sq) {
if (isSolved) return;
const piece = boardState[sq];
if (selectedSquare) {
const validMoves = getValidMoves(selectedSquare);
if (validMoves.includes(sq)) {
// Execute Move!
executeMove(selectedSquare, sq);
selectedSquare = null;
return;
}
}
// Select new piece if black
if (piece && piece.startsWith('b')) {
selectedSquare = sq;
if (window.soundFX) window.soundFX.playStoneSlide();
renderBoard();
} else {
selectedSquare = null;
renderBoard();
}
}
async function executeMove(fromSq, toSq) {
const piece = boardState[fromSq];
const capturedPiece = boardState[toSq];
delete boardState[fromSq];
boardState[toSq] = piece;
if (window.soundFX) {
window.soundFX.playStoneSlide();
if (capturedPiece) {
window.soundFX.playPoisonSizzle(); // Crushing sound
}
}
renderBoard();
// Check if winning move:
// Winning move 1: Knight moves to f2+ (Smothered mate / Royal fork!)
// Winning move 2: Queen captures h2# (Schachmatt!)
if (piece === 'bN' && toSq === 'f2') {
if (moveHistoryEl) {
const li = document.createElement('li');
li.innerHTML = `<strong>1... ♞f2+</strong> (Springer-Schach! Weißer König erstarrt!)`;
moveHistoryEl.appendChild(li);
}
triggerWin("♞f2+", "Rons Springer zerschellt die weiße Verteidigung! Der weiße König hat kein Feld mehr und die weiße Königin muss kapitulieren!");
} else if (piece === 'bQ' && toSq === 'h2') {
if (moveHistoryEl) {
const li = document.createElement('li');
li.innerHTML = `<strong>1... ♛xh2#</strong> (Schachmatt! Die schwarze Dame schlägt zu!)`;
moveHistoryEl.appendChild(li);
}
triggerWin("♛xh2#", "Schachmatt! Mit kaltblütiger Präzision fällt der weiße König in Trümmer!");
} else {
if (moveHistoryEl) {
const li = document.createElement('li');
li.innerHTML = `Zug: ${piece} nach ${toSq}. Weiß pariert den Angriff. Versuche es erneut!`;
moveHistoryEl.appendChild(li);
}
if (statusMsg) {
statusMsg.innerHTML = "⚠️ <em>Kein Schachmatt! Die weißen Figuren schlagen zurück. Drücke auf 'Zurücksetzen' und versuche es erneut!</em>";
statusMsg.style.color = "#e67e22";
}
}
}
async function triggerWin(moveNotation, explanation) {
isSolved = true;
if (window.soundFX) window.soundFX.playMagicSuccess();
if (statusMsg) {
statusMsg.innerHTML = `🏆 <strong>${moveNotation} — SCHACHMATT!</strong> Die gigantischen weißen Steinfiguren senken ihre Schwerter!`;
statusMsg.style.color = "#2ecc71";
}
try {
const res = await fetch('/api/chess/complete', { method: 'POST' });
const data = await res.json();
if (data.new_item && window.notifyNewItem) {
window.notifyNewItem(data.new_item);
}
} catch (e) {
console.error(e);
}
modalTitle.innerText = "♟️ Zauberschach Gemeistert!";
modalText.innerHTML = `
<strong>${moveNotation} — Grandios kombiniert!</strong><br><br>
${explanation}<br><br>
Die weiße Königin wirft ihr Schwert zu Boden. Die Riesen-Schachfiguren weichen zur Seite und die Tür zur <strong>Flammenkammer (Snapes Flaschenrätsel)</strong> steht weit offen!<br><br>
🎒 <em>Ein massiver schwarzer Springer wurde deinem Zauberbeutel hinzugefügt!</em>
`;
modalBtn.innerText = "Weiter zu Snapes Flammenkammer 🧪";
modalBtn.onclick = () => { window.location.href = '/bottles'; };
modalBackdrop.classList.add('show');
}
if (btnResetChess) {
btnResetChess.addEventListener('click', () => {
if (window.soundFX) window.soundFX.playStoneSlide();
initBoard();
});
}
initBoard();
})();