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
Binary file not shown.
+46 -4
View File
@@ -64,6 +64,14 @@ ALL_ITEMS = {
"action": "examine_key",
"action_label": "Schloss prüfen 🔍"
},
"chess_knight": {
"id": "chess_knight",
"name": "Schwarzer Springer",
"icon": "♟️",
"desc": "Ein massiver schwarzer Springer aus McGonagalls verzaubertem Riesenschach. Hat das weiße Heer geschlagen!",
"action": "chess_piece",
"action_label": "Stein berühren ⚡"
},
"philosophers_stone": {
"id": "philosophers_stone",
"name": "Stein der Weisen",
@@ -90,6 +98,7 @@ def get_session_state(request: Request) -> dict:
"broom_solved": False,
"broom_score": 0,
"broom_time": 0,
"chess_solved": False,
"bottles_solved": False,
"sliding_solved": False,
"started_at": datetime.datetime.utcnow().isoformat(),
@@ -97,16 +106,19 @@ def get_session_state(request: Request) -> dict:
"lang": "de"
}
else:
# Ensure new fields exist for existing sessions
state = request.session["escape"]
if "inventory" not in state:
state["inventory"] = ["wand", "broom", "snape_scroll"]
if state.get("broom_solved"):
state["inventory"].append("silver_key")
if state.get("chess_solved"):
state["inventory"].append("chess_knight")
if state.get("bottles_solved"):
state["inventory"].append("philosophers_stone")
if "house" not in state:
state["house"] = "gryffindor"
if "chess_solved" not in state:
state["chess_solved"] = False
request.session["escape"] = state
return request.session["escape"]
@@ -147,6 +159,18 @@ async def page_broom(request: Request):
context=get_template_context(request, state, lang)
)
@app.get("/chess", response_class=HTMLResponse)
async def page_chess(request: Request):
state = get_session_state(request)
lang = request.query_params.get("lang", state.get("lang", "de"))
state["lang"] = lang
request.session["escape"] = state
return templates.TemplateResponse(
request=request,
name="chess.html",
context=get_template_context(request, state, lang)
)
@app.get("/bottles", response_class=HTMLResponse)
async def page_bottles(request: Request):
state = get_session_state(request)
@@ -199,6 +223,7 @@ async def api_get_inventory(request: Request):
"house": house_info,
"score": state.get("broom_score", 0),
"broom_solved": state.get("broom_solved", False),
"chess_solved": state.get("chess_solved", False),
"bottles_solved": state.get("bottles_solved", False),
"sliding_solved": state.get("sliding_solved", False)
})
@@ -226,6 +251,7 @@ async def api_reset_state(request: Request):
"broom_solved": False,
"broom_score": 0,
"broom_time": 0,
"chess_solved": False,
"bottles_solved": False,
"sliding_solved": False,
"started_at": datetime.datetime.utcnow().isoformat(),
@@ -244,7 +270,7 @@ async def api_broom_complete(request: Request):
state["broom_solved"] = True
state["broom_score"] = int(data.get("score", 100))
state["broom_time"] = int(data.get("time_taken", 30))
# Add Silver Key to Inventory!
# Add Silver Key to Inventory
if "inventory" not in state:
state["inventory"] = ["wand", "broom", "snape_scroll"]
if "silver_key" not in state["inventory"]:
@@ -252,11 +278,27 @@ async def api_broom_complete(request: Request):
request.session["escape"] = state
return JSONResponse({
"success": True,
"message": "Der Schlüssel passt ins Schloss! Die Tür zur Flammenkammer steht offen!",
"message": "Der Schlüssel passt ins Schloss! Die Tür zu McGonagalls Zauberschach steht offen!",
"new_item": ALL_ITEMS["silver_key"],
"state": state
})
@app.post("/api/chess/complete")
async def api_chess_complete(request: Request):
state = get_session_state(request)
state["chess_solved"] = True
if "inventory" not in state:
state["inventory"] = ["wand", "broom", "snape_scroll"]
if "chess_knight" not in state["inventory"]:
state["inventory"].append("chess_knight")
request.session["escape"] = state
return JSONResponse({
"success": True,
"message": "Schachmatt! Die weiße Königin senkt ihr Schwert. Der Weg zur Flammenkammer ist frei!",
"new_item": ALL_ITEMS["chess_knight"],
"state": state
})
@app.post("/api/bottles/drink")
async def api_bottles_drink(request: Request):
try:
@@ -307,7 +349,7 @@ async def api_bottles_drink(request: Request):
"result": "success_back",
"content": content,
"message": msg_de if lang == "de" else msg_en,
"next_url": "/broom"
"next_url": "/chess"
})
else:
msg_de = "Dieser Trank schützt vor den violetten Flammen, führt dich aber nicht vorwärts durch das schwarze Feuer!"
+210 -12
View File
@@ -808,7 +808,10 @@ body {
#broomCanvas {
display: block;
width: 100%;
height: 480px;
touch-action: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
cursor: crosshair;
}
@@ -825,34 +828,128 @@ body {
gap: 0.5rem;
}
.mobile-controls {
display: none;
margin-top: 1rem;
/* Mobile Flight Cockpit & D-Pad */
.mobile-flight-cockpit {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
gap: 1rem;
margin-top: 1.25rem;
padding: 1rem;
background: rgba(18, 15, 26, 0.9);
border: 2px solid rgba(229, 185, 72, 0.35);
border-radius: 14px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
user-select: none;
-webkit-user-select: none;
}
.dpad-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.dpad-middle {
display: flex;
align-items: center;
gap: 4px;
}
.dpad-btn {
width: 48px;
height: 48px;
background: linear-gradient(145deg, #2a2238, #161220);
border: 2px solid var(--gold);
color: var(--gold);
border-radius: 8px;
font-size: 1.3rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.8rem;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
transition: transform 0.1s, background 0.1s;
touch-action: none;
-webkit-tap-highlight-color: transparent;
}
.dpad-btn:active {
transform: scale(0.92);
background: var(--gold);
color: #000;
}
.dpad-core {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
color: var(--gold);
opacity: 0.7;
}
.mobile-flight-actions {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
flex: 1;
max-width: 200px;
}
.dpad-boost-btn {
width: 100%;
padding: 0.9rem 1.25rem;
background: linear-gradient(135deg, #d4af37, #f39c12);
color: #1a1100;
border: 2px solid #fff;
border-radius: 12px;
font-size: 1.15rem;
font-weight: bold;
cursor: pointer;
box-shadow: 0 0 20px var(--gold-glow);
touch-action: none;
-webkit-tap-highlight-color: transparent;
transition: transform 0.1s;
}
.dpad-boost-btn:active {
transform: scale(0.95);
filter: brightness(1.2);
}
.touch-hint {
font-size: 0.78rem;
color: var(--text-dim);
text-align: center;
line-height: 1.4;
}
@media (max-width: 768px) {
.mobile-controls {
display: flex;
}
.flame-wall-container {
flex-direction: column;
}
#broomCanvas {
height: 360px;
}
.brand-title {
font-size: 1.15rem;
}
.bag-label {
display: none; /* Icon and badge only on small screens for ultra compact header */
display: none;
}
.btn-bag {
padding: 0.35rem 0.6rem;
}
}
.btn-bag {
padding: 0.35rem 0.6rem;
}
}
/* Modal Feedback */
.modal-backdrop {
position: fixed;
@@ -992,3 +1089,104 @@ body {
.footer a:hover {
text-decoration: underline;
}
/* McGonagall's Wizard Chess (Zauberschach) */
.chess-board-wrapper {
max-width: 440px;
margin: 1.25rem auto;
background: #171322;
border: 3px solid var(--gold);
border-radius: 12px;
padding: 0.75rem;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.9), inset 0 0 20px rgba(0, 0, 0, 0.8);
}
.chess-board {
display: grid;
grid-template-columns: repeat(8, 1fr);
width: 100%;
aspect-ratio: 1 / 1;
border: 2px solid #5a4b38;
border-radius: 6px;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
.chess-sq {
display: flex;
align-items: center;
justify-content: center;
position: relative;
cursor: pointer;
transition: background 0.15s;
}
.chess-sq.light {
background: #e8d8be;
}
.chess-sq.dark {
background: #5c4b37;
}
.chess-sq.selected {
background: #d4af37 !important;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.7);
}
.chess-piece {
font-size: 2.2rem;
line-height: 1;
transition: transform 0.15s;
pointer-events: none;
}
.white-piece {
color: #ffffff;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8));
text-shadow: 0 0 2px #333;
}
.black-piece {
color: #1a1226;
filter: drop-shadow(0 2px 4px rgba(229, 185, 72, 0.4));
text-shadow: 0 0 3px #e5b948;
}
.chess-sq:hover .chess-piece {
transform: scale(1.1);
}
.move-dot {
width: 14px;
height: 14px;
background: rgba(229, 185, 72, 0.75);
border-radius: 50%;
position: absolute;
box-shadow: 0 0 8px var(--gold);
pointer-events: none;
}
.move-capture-ring {
width: 80%;
height: 80%;
border: 3px solid rgba(231, 76, 60, 0.85);
border-radius: 50%;
position: absolute;
box-shadow: 0 0 10px #e74c3c;
pointer-events: none;
animation: ringPulse 1s infinite alternate;
}
@keyframes ringPulse {
0% { transform: scale(0.9); opacity: 0.7; }
100% { transform: scale(1.05); opacity: 1; }
}
@media (max-width: 480px) {
.chess-piece {
font-size: 1.8rem;
}
}
+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();
})();
+13 -1
View File
@@ -122,6 +122,10 @@
<span>🧹 Besenflug:</span>
<span>{% if state.broom_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %}</span>
</div>
<div class="stat-row">
<span>♟️ Zauberschach:</span>
<span>{% if state.chess_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %}</span>
</div>
<div class="stat-row">
<span>🧪 Snapes Flammen:</span>
<span>{% if state.bottles_solved %}✅ Gelöst{% else %}⏳ Offen{% endif %}</span>
@@ -150,13 +154,21 @@
<small>{% if state.broom_solved %}✅ Gemeistert{% else %}🗝️ Geflügelte Schlüssel jagen{% endif %}</small>
</div>
</a>
<a href="/chess" class="room-nav-item {% if request.url.path == '/chess' %}current{% endif %}">
<span class="room-icon">♟️</span>
<div class="room-info">
<strong>Kammer II: McGonagalls Schach</strong>
<small>{% if state.chess_solved %}✅ Gemeistert{% else %}⚔️ Riesen-Zauberschach{% endif %}</small>
</div>
</a>
<a href="/bottles" class="room-nav-item {% if request.url.path == '/bottles' %}current{% endif %}">
<span class="room-icon">🧪</span>
<div class="room-info">
<strong>Kammer II: Snapes Flaschen</strong>
<strong>Kammer III: Snapes Flaschen</strong>
<small>{% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Die Flammenkammer{% endif %}</small>
</div>
</a>
<a href="/sliding" class="room-nav-item {% if request.url.path == '/sliding' %}current{% endif %}">
<span class="room-icon">🧩</span>
<div class="room-info">
+25 -10
View File
@@ -16,33 +16,48 @@
<div class="game-hud">
<div>⏱️ Zeit: <strong id="timerVal">0s</strong></div>
<div>✨ Punkte: <strong id="scoreVal">0</strong></div>
<div>🎯 Mission: <span id="missionText">Fange den Schlüssel mit geknicktem Flügel!</span></div>
<div>🎯 Ziel: <span id="missionText">Schlüssel fangen!</span></div>
</div>
<canvas id="broomCanvas"></canvas>
</div>
<!-- Mobile Touch Controls -->
<div class="mobile-controls">
<button id="mBtnUp" class="btn-control" style="font-size: 1.2rem; padding: 0.8rem 1.4rem;">▲ Hoch</button>
<button id="mBtnBoost" class="btn-control" style="font-size: 1.2rem; padding: 0.8rem 1.4rem; background: var(--gold); color: #000;">⚡ Turbo</button>
<button id="mBtnDown" class="btn-control" style="font-size: 1.2rem; padding: 0.8rem 1.4rem;">▼ Runter</button>
<!-- Mobile Flight Cockpit & D-Pad -->
<div class="mobile-flight-cockpit">
<div class="dpad-box">
<button type="button" id="mBtnUp" class="dpad-btn dpad-up" aria-label="Hoch">▲</button>
<div class="dpad-middle">
<button type="button" id="mBtnLeft" class="dpad-btn dpad-left" aria-label="Links">◀</button>
<div class="dpad-core">✨</div>
<button type="button" id="mBtnRight" class="dpad-btn dpad-right" aria-label="Rechts">▶</button>
</div>
<button type="button" id="mBtnDown" class="dpad-btn dpad-down" aria-label="Runter">▼</button>
</div>
<div class="mobile-flight-actions">
<button type="button" id="mBtnBoost" class="dpad-boost-btn">
⚡ TURBO
</button>
<div class="touch-hint">
💡 <em>Tipp: Du kannst auch direkt mit dem Finger über das Spielfeld wischen!</em>
</div>
</div>
</div>
<div class="dungeon-card" style="margin-top: 1.5rem;">
<p id="statusMsg" style="text-align: center; font-size: 1.15rem; color: var(--gold); margin-bottom: 0.8rem;">
🎮 <strong>Steuerung:</strong> Pfeiltasten / WASD / Mausbewegung zum Fliegen · <strong>Leertaste / Klick</strong> für Besen-Turbo!
🎮 <strong>Steuerung:</strong> Pfeiltasten / WASD / Touch-Wischen zum Fliegen · <strong>Leertaste / Turbo</strong> für Besen-Beschleunigung!
</p>
<p style="text-align: center; color: var(--text-dim); font-size: 0.95rem;">
Fliege durch den Raum, jage den ausweichenden Silberschlüssel mit dem verletzten Flügel und fliege nach dem Fang zur Tür ganz rechts!
Fange den großen Silberschlüssel mit dem geknickten Flügel und fliege zur schweren Holztür ganz rechts!
</p>
</div>
<div style="text-align: center; margin-top: 1rem;">
<a href="/bottles" class="nav-link" style="color: var(--text-dim);">Direkt weiter zu Snapes Flaschenkammer 🧪 ➔</a>
<a href="/chess" class="nav-link" style="color: var(--text-dim);">Direkt weiter zu McGonagalls Zauberschach ♟️ ➔</a>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/broom.js?v=1.0"></script>
<script src="/static/js/broom.js?v=2.0"></script>
{% endblock %}
+68
View File
@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Kammer II: McGonagalls Zauberschach · Hogwarts Escape{% endblock %}
{% block content %}
<div class="parchment-scroll">
<h1 class="parchment-title">♟️ Kammer II: McGonagalls Zauberschach ⚔️</h1>
<p class="riddle-poem" style="font-size: 1.25rem; margin: 0.5rem 0;">
"Wir müssen uns den Weg quer durch das Zimmer freispielen!", rief Ron.<br>
Hinter den weißen Riesenfiguren liegt die schwere Eichentür zur Flammenkammer.<br>
Übernimm die Kontrolle über die schwarzen Steine und setze den weißen König schachmatt!
</p>
</div>
<div class="dungeon-card" style="text-align: center;">
<div id="chessStatus" style="font-size: 1.15rem; color: var(--gold); margin-bottom: 1rem; min-height: 28px;">
🧙‍♂️ Wähle deinen schwarzen Springer oder deine Dame und führe den Siegeszug aus!
</div>
<!-- 8x8 Chess Board Container -->
<div class="chess-board-wrapper">
<div id="chessBoard" class="chess-board">
<!-- Rendered dynamically by chess.js -->
</div>
</div>
<div style="margin-top: 1.25rem; display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap;">
<button type="button" id="btnResetChess" class="btn-control" style="font-size: 0.95rem; padding: 0.6rem 1.2rem;">
🔄 Stellung zurücksetzen
</button>
<a href="/bottles" class="btn-magical">
🧪 Zu Snapes Flaschenkammer ➔
</a>
</div>
<!-- Move History Log -->
<div style="margin-top: 1.25rem; text-align: left; background: rgba(15, 12, 22, 0.7); border: 1px solid rgba(229, 185, 72, 0.25); border-radius: 8px; padding: 0.8rem 1rem;">
<strong style="color: var(--gold); font-size: 0.9rem;">📜 Zug-Protokoll:</strong>
<ul id="moveHistory" style="list-style: none; margin-top: 0.4rem; font-size: 0.85rem; color: var(--text-dim); line-height: 1.6;">
<li>Partie eröffnet: Schwarz am Zug!</li>
</ul>
</div>
</div>
<!-- Tactical Hint Accordion -->
<div class="dungeon-card">
<details>
<summary style="cursor: pointer; font-size: 1.15rem; color: var(--gold); font-weight: bold;">
💡 Rons taktischer Schach-Tipp (Hier klicken)
</summary>
<div style="margin-top: 0.75rem; line-height: 1.7; color: var(--text-light); font-size: 0.95rem;">
<p>
Der weiße König steht in der Ecke auf <strong>h1</strong>, eingeklemmt von seinen eigenen Bauern auf g2 und h2.
</p>
<p style="margin-top: 0.5rem;">
🎯 <strong>Möglichkeit 1:</strong> Führe mit dem schwarzen Springer auf <strong>g4</strong> ein verheerendes Schachgebot auf <strong>f2</strong> aus (♞f2+) – Rons klassisches Springer-Opfer!
</p>
<p style="margin-top: 0.5rem;">
🎯 <strong>Möglichkeit 2:</strong> Schlage mit der schwarzen Dame auf <strong>h4</strong> direkt den Bauern auf <strong>h2</strong> (♛xh2#) – augenblickliches Schachmatt mit Springer-Deckung!
</p>
</div>
</details>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/chess.js?v=1.0"></script>
{% endblock %}
+17 -2
View File
@@ -35,13 +35,27 @@
<a href="/broom" class="btn-magical">Auf den Besen schwingen 🧹</a>
</div>
<!-- Chamber 2: Snape's Potions -->
<!-- Chamber 2: McGonagall's Wizard Chess (NEW!) -->
<div class="chamber-card">
<span class="chamber-badge {% if state.chess_solved %}solved{% endif %}">
{% if state.chess_solved %}✅ Gemeistert{% else %}⚔️ Bereit{% endif %}
</span>
<div class="chamber-icon">♟️⚔️</div>
<h3 class="chamber-title">Kammer II: McGonagalls Schach</h3>
<p class="chamber-desc">
Das gigantische, lebendige Zauberschach. Übernimm Rons Rolle als schwarzer Springer,
überliste die weiße Königin und setze den feindlichen König schachmatt!
</p>
<a href="/chess" class="btn-magical">Schachbrett betreten ♟️</a>
</div>
<!-- Chamber 3: Snape's Potions -->
<div class="chamber-card">
<span class="chamber-badge {% if state.bottles_solved %}solved{% endif %}">
{% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Bereit{% endif %}
</span>
<div class="chamber-icon">🧪🔥</div>
<h3 class="chamber-title">Kammer II: Snapes Flaschenrätsel</h3>
<h3 class="chamber-title">Kammer III: Snapes Flaschen</h3>
<p class="chamber-desc">
Schwarze Flammen versperren den Weg zum Stein, violette Flammen den Rückweg.
Auf dem Tisch stehen 7 Flaschen und Snapes unerbittliches Logik-Gedicht.
@@ -50,6 +64,7 @@
<a href="/bottles" class="btn-magical">Flammenkammer betreten 🧪</a>
</div>
<!-- Bonus: Alchemy Sliding Puzzle -->
<div class="chamber-card">
<span class="chamber-badge {% if state.sliding_solved %}solved{% endif %}">
+13 -1
View File
@@ -15,6 +15,10 @@ def test_endpoints():
assert res.status_code == 200
assert "Der Besenflug" in res.text
res = client.get("/chess")
assert res.status_code == 200
assert "McGonagalls Zauberschach" in res.text
res = client.get("/bottles")
assert res.status_code == 200
assert "Snapes Flaschenrätsel" in res.text
@@ -50,6 +54,14 @@ def test_endpoints():
assert data["success"] is True
assert "silver_key" in data["state"]["inventory"]
# Test API Chess complete (adds chess_knight)
res = client.post("/api/chess/complete")
assert res.status_code == 200
chess_data = res.json()
assert chess_data["success"] is True
assert chess_data["new_item"]["id"] == "chess_knight"
assert "chess_knight" in chess_data["state"]["inventory"]
# Test API Bottles drink: Ahead / Black fire (Bottle 3 - Dwarf, adds philosophers_stone)
res = client.post("/api/bottles/drink", json={"bottle": 3, "gate": "ahead"})
assert res.status_code == 200
@@ -63,7 +75,7 @@ def test_endpoints():
assert res.status_code == 200
assert res.json()["success"] is True
print("ALL INVENTORY & GAMEPLAY TESTS PASSED! 🎒✨")
print("ALL TESTS (INCLUDING CHESS & MOBILE FLIGHT) PASSED! ♟️🧹✨")
if __name__ == "__main__":
test_endpoints()