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": "examine_key",
"action_label": "Schloss prüfen 🔍" "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": { "philosophers_stone": {
"id": "philosophers_stone", "id": "philosophers_stone",
"name": "Stein der Weisen", "name": "Stein der Weisen",
@@ -90,6 +98,7 @@ def get_session_state(request: Request) -> dict:
"broom_solved": False, "broom_solved": False,
"broom_score": 0, "broom_score": 0,
"broom_time": 0, "broom_time": 0,
"chess_solved": False,
"bottles_solved": False, "bottles_solved": False,
"sliding_solved": False, "sliding_solved": False,
"started_at": datetime.datetime.utcnow().isoformat(), "started_at": datetime.datetime.utcnow().isoformat(),
@@ -97,16 +106,19 @@ def get_session_state(request: Request) -> dict:
"lang": "de" "lang": "de"
} }
else: else:
# Ensure new fields exist for existing sessions
state = request.session["escape"] state = request.session["escape"]
if "inventory" not in state: if "inventory" not in state:
state["inventory"] = ["wand", "broom", "snape_scroll"] state["inventory"] = ["wand", "broom", "snape_scroll"]
if state.get("broom_solved"): if state.get("broom_solved"):
state["inventory"].append("silver_key") state["inventory"].append("silver_key")
if state.get("chess_solved"):
state["inventory"].append("chess_knight")
if state.get("bottles_solved"): if state.get("bottles_solved"):
state["inventory"].append("philosophers_stone") state["inventory"].append("philosophers_stone")
if "house" not in state: if "house" not in state:
state["house"] = "gryffindor" state["house"] = "gryffindor"
if "chess_solved" not in state:
state["chess_solved"] = False
request.session["escape"] = state request.session["escape"] = state
return request.session["escape"] return request.session["escape"]
@@ -147,6 +159,18 @@ async def page_broom(request: Request):
context=get_template_context(request, state, lang) 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) @app.get("/bottles", response_class=HTMLResponse)
async def page_bottles(request: Request): async def page_bottles(request: Request):
state = get_session_state(request) state = get_session_state(request)
@@ -199,6 +223,7 @@ async def api_get_inventory(request: Request):
"house": house_info, "house": house_info,
"score": state.get("broom_score", 0), "score": state.get("broom_score", 0),
"broom_solved": state.get("broom_solved", False), "broom_solved": state.get("broom_solved", False),
"chess_solved": state.get("chess_solved", False),
"bottles_solved": state.get("bottles_solved", False), "bottles_solved": state.get("bottles_solved", False),
"sliding_solved": state.get("sliding_solved", False) "sliding_solved": state.get("sliding_solved", False)
}) })
@@ -226,6 +251,7 @@ async def api_reset_state(request: Request):
"broom_solved": False, "broom_solved": False,
"broom_score": 0, "broom_score": 0,
"broom_time": 0, "broom_time": 0,
"chess_solved": False,
"bottles_solved": False, "bottles_solved": False,
"sliding_solved": False, "sliding_solved": False,
"started_at": datetime.datetime.utcnow().isoformat(), "started_at": datetime.datetime.utcnow().isoformat(),
@@ -244,7 +270,7 @@ async def api_broom_complete(request: Request):
state["broom_solved"] = True state["broom_solved"] = True
state["broom_score"] = int(data.get("score", 100)) state["broom_score"] = int(data.get("score", 100))
state["broom_time"] = int(data.get("time_taken", 30)) state["broom_time"] = int(data.get("time_taken", 30))
# Add Silver Key to Inventory! # Add Silver Key to Inventory
if "inventory" not in state: if "inventory" not in state:
state["inventory"] = ["wand", "broom", "snape_scroll"] state["inventory"] = ["wand", "broom", "snape_scroll"]
if "silver_key" not in state["inventory"]: if "silver_key" not in state["inventory"]:
@@ -252,11 +278,27 @@ async def api_broom_complete(request: Request):
request.session["escape"] = state request.session["escape"] = state
return JSONResponse({ return JSONResponse({
"success": True, "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"], "new_item": ALL_ITEMS["silver_key"],
"state": state "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") @app.post("/api/bottles/drink")
async def api_bottles_drink(request: Request): async def api_bottles_drink(request: Request):
try: try:
@@ -307,7 +349,7 @@ async def api_bottles_drink(request: Request):
"result": "success_back", "result": "success_back",
"content": content, "content": content,
"message": msg_de if lang == "de" else msg_en, "message": msg_de if lang == "de" else msg_en,
"next_url": "/broom" "next_url": "/chess"
}) })
else: else:
msg_de = "Dieser Trank schützt vor den violetten Flammen, führt dich aber nicht vorwärts durch das schwarze Feuer!" 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 { #broomCanvas {
display: block; display: block;
width: 100%; width: 100%;
height: 480px; touch-action: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
cursor: crosshair; cursor: crosshair;
} }
@@ -825,34 +828,128 @@ body {
gap: 0.5rem; gap: 0.5rem;
} }
.mobile-controls { /* Mobile Flight Cockpit & D-Pad */
display: none; .mobile-flight-cockpit {
margin-top: 1rem; 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; 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) { @media (max-width: 768px) {
.mobile-controls {
display: flex;
}
.flame-wall-container { .flame-wall-container {
flex-direction: column; flex-direction: column;
} }
#broomCanvas {
height: 360px;
}
.brand-title { .brand-title {
font-size: 1.15rem; font-size: 1.15rem;
} }
.bag-label { .bag-label {
display: none; /* Icon and badge only on small screens for ultra compact header */ display: none;
} }
.btn-bag { .btn-bag {
padding: 0.35rem 0.6rem; padding: 0.35rem 0.6rem;
} }
} }
.btn-bag {
padding: 0.35rem 0.6rem;
}
}
/* Modal Feedback */ /* Modal Feedback */
.modal-backdrop { .modal-backdrop {
position: fixed; position: fixed;
@@ -992,3 +1089,104 @@ body {
.footer a:hover { .footer a:hover {
text-decoration: underline; 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() { (function() {
const canvas = document.getElementById('broomCanvas'); const canvas = document.getElementById('broomCanvas');
if (!canvas) return; if (!canvas) return;
@@ -13,15 +13,28 @@
const modalText = document.getElementById('modalText'); const modalText = document.getElementById('modalText');
const modalBtn = document.getElementById('modalBtn'); const modalBtn = document.getElementById('modalBtn');
let width, height; let width = 800, height = 480;
function resize() { function resize() {
width = canvas.parentElement.clientWidth; const rect = canvas.getBoundingClientRect();
height = Math.min(520, window.innerHeight * 0.65); 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.width = width;
canvas.height = height; 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 // Game State
let gameRunning = true; let gameRunning = true;
@@ -33,10 +46,10 @@
// Player (Broomstick) // Player (Broomstick)
const player = { const player = {
x: 80, x: 80,
y: height / 2, y: 200,
vx: 0, vx: 0,
vy: 0, vy: 0,
speed: 5.5, speed: 6.0,
boost: 1.0, boost: 1.0,
angle: 0, angle: 0,
trail: [] trail: []
@@ -44,26 +57,22 @@
// Keys Swarm // Keys Swarm
const keys = []; const keys = [];
const NUM_KEYS = 24; const NUM_KEYS = 22;
// Master Key (Damaged wing silver key) // Master Key (Damaged wing silver key)
const masterKey = { const masterKey = {
x: width - 150, x: 600,
y: height / 2, y: 200,
vx: -2, vx: -2.5,
vy: 1.5, vy: 1.5,
wingPhase: 0, wingPhase: 0,
caught: false, caught: false,
size: 16 size: 17
}; };
// Target Door // Initialize Canvas Size
const door = { window.addEventListener('resize', resize);
x: width - 60, resize();
y: height / 2 - 45,
w: 45,
h: 90
};
// Spawn decoy keys // Spawn decoy keys
for (let i = 0; i < NUM_KEYS; i++) { for (let i = 0; i < NUM_KEYS; i++) {
@@ -79,53 +88,106 @@
}); });
} }
// Controls // Keyboard Controls
const keysDown = {}; const keysDown = {};
window.addEventListener('keydown', e => { window.addEventListener('keydown', e => {
keysDown[e.key.toLowerCase()] = true; 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 => { window.addEventListener('keyup', e => {
keysDown[e.key.toLowerCase()] = false; keysDown[e.key.toLowerCase()] = false;
if (e.key === ' ' || e.key === 'Shift') player.boost = 1.0; if (e.key === ' ' || e.key === 'Shift') player.boost = 1.0;
}); });
// Pointer / Touch control // Touch & Pointer Steering
let pointerTarget = null; let pointerTarget = null;
canvas.addEventListener('pointermove', e => {
function getCanvasCoords(clientX, clientY) {
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
pointerTarget = { return {
x: e.clientX - rect.left, x: (clientX - rect.left) * (canvas.width / rect.width),
y: e.clientY - rect.top y: (clientY - rect.top) * (canvas.height / rect.height)
}; };
}); }
canvas.addEventListener('pointerdown', e => {
player.boost = 1.6; // 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(); 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 => {
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', () => { canvas.addEventListener('pointerup', () => {
player.boost = 1.0;
});
canvas.addEventListener('pointerleave', () => {
pointerTarget = null; pointerTarget = null;
player.boost = 1.0; player.boost = 1.0;
}); });
// Mobile buttons // Mobile D-Pad Buttons
const btnUp = document.getElementById('mBtnUp'); function bindDpad(btnId, keyName) {
const btnDown = document.getElementById('mBtnDown'); 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'); 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) { if (btnBoost) {
btnBoost.addEventListener('pointerdown', () => { player.boost = 1.8; if (window.soundFX) window.soundFX.playWhoosh(); }); const startBoost = (e) => {
btnBoost.addEventListener('pointerup', () => { player.boost = 1.0; }); 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() { function update() {
@@ -134,74 +196,73 @@
const elapsedSec = Math.floor((Date.now() - startTime) / 1000); const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
if (timerVal) timerVal.innerText = elapsedSec + 's'; if (timerVal) timerVal.innerText = elapsedSec + 's';
// Keyboard Movement // Keyboard & D-Pad Movement
let ax = 0, ay = 0; let ax = 0, ay = 0;
if (keysDown['arrowup'] || keysDown['w']) ay -= 1; if (keysDown['arrowup'] || keysDown['w']) ay -= 1;
if (keysDown['arrowdown'] || keysDown['s']) ay += 1; if (keysDown['arrowdown'] || keysDown['s']) ay += 1;
if (keysDown['arrowleft'] || keysDown['a']) ax -= 1; if (keysDown['arrowleft'] || keysDown['a']) ax -= 1;
if (keysDown['arrowright'] || keysDown['d']) ax += 1; if (keysDown['arrowright'] || keysDown['d']) ax += 1;
// Pointer steer // Pointer steer (Touch / Mouse direct following)
if (pointerTarget) { if (pointerTarget) {
const dx = pointerTarget.x - player.x; const dx = pointerTarget.x - player.x;
const dy = pointerTarget.y - player.y; const dy = pointerTarget.y - player.y;
const dist = Math.hypot(dx, dy); const dist = Math.hypot(dx, dy);
if (dist > 15) { if (dist > 12) {
ax = dx / dist; ax = dx / dist;
ay = dy / dist; ay = dy / dist;
} }
} }
const curSpeed = player.speed * player.boost; const curSpeed = player.speed * player.boost;
player.vx = player.vx * 0.85 + ax * curSpeed * 0.2; player.vx = player.vx * 0.82 + ax * curSpeed * 0.22;
player.vy = player.vy * 0.85 + ay * curSpeed * 0.2; player.vy = player.vy * 0.82 + ay * curSpeed * 0.22;
player.x += player.vx; player.x += player.vx;
player.y += player.vy; player.y += player.vy;
// Bounds // 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.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 // Particles trail
player.trail.push({ x: player.x - 20, y: player.y, alpha: 1, size: Math.random() * 4 + 2 }); player.trail.push({ x: player.x - 22, y: player.y, alpha: 1, size: Math.random() * 4 + 2 });
if (player.trail.length > 20) player.trail.shift(); if (player.trail.length > 22) player.trail.shift();
player.trail.forEach(p => { p.alpha *= 0.9; p.x -= 2; }); player.trail.forEach(p => { p.alpha *= 0.88; p.x -= 2; });
// Update decoy keys // Update decoy keys
keys.forEach(k => { keys.forEach(k => {
k.wingPhase += k.wingSpeed; k.wingPhase += k.wingSpeed;
k.x += k.vx; k.x += k.vx;
k.y += k.vy; 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; if (k.y < 30 || k.y > height - 30) k.vy *= -1;
}); });
// Update Master Key // Update Master Key
if (!masterKey.caught) { if (!masterKey.caught) {
masterKey.wingPhase += 0.35; masterKey.wingPhase += 0.35;
// Evasive maneuver if player is close
const dToPlayer = Math.hypot(player.x - masterKey.x, player.y - masterKey.y); const dToPlayer = Math.hypot(player.x - masterKey.x, player.y - masterKey.y);
if (dToPlayer < 140) { if (dToPlayer < 150) {
masterKey.vx += (masterKey.x - player.x) * 0.04; masterKey.vx += (masterKey.x - player.x) * 0.045;
masterKey.vy += (masterKey.y - player.y) * 0.04; masterKey.vy += (masterKey.y - player.y) * 0.045;
} else { } else {
masterKey.vx += (Math.random() - 0.5) * 0.8; masterKey.vx += (Math.random() - 0.5) * 0.9;
masterKey.vy += (Math.random() - 0.5) * 0.8; masterKey.vy += (Math.random() - 0.5) * 0.9;
} }
masterKey.vx = Math.max(-5, Math.min(5, masterKey.vx * 0.95)); masterKey.vx = Math.max(-5.5, Math.min(5.5, masterKey.vx * 0.95));
masterKey.vy = Math.max(-4, Math.min(4, masterKey.vy * 0.95)); masterKey.vy = Math.max(-4.5, Math.min(4.5, masterKey.vy * 0.95));
masterKey.x += masterKey.vx; masterKey.x += masterKey.vx;
masterKey.y += masterKey.vy; 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)); masterKey.y = Math.max(40, Math.min(height - 40, masterKey.y));
// Catch check // Catch check (generous hit box for mobile thumbs: 42px)
if (dToPlayer < 35) { if (dToPlayer < 42) {
masterKey.caught = true; masterKey.caught = true;
keyCaught = true; keyCaught = true;
score += 500; score += 500;
@@ -211,11 +272,10 @@
window.notifyNewItem({ icon: "🗝️", name: "Geknickter Silberschlüssel" }); window.notifyNewItem({ icon: "🗝️", name: "Geknickter Silberschlüssel" });
} }
if (statusMsg) { 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"; statusMsg.style.color = "#2ecc71";
} }
} }
} else { } else {
// Master key follows player // Master key follows player
masterKey.x = player.x + 10; masterKey.x = player.x + 10;
@@ -223,7 +283,7 @@
// Check if player reached the door // Check if player reached the door
const dToDoor = Math.hypot(player.x - (door.x + door.w / 2), player.y - (door.y + door.h / 2)); 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; doorUnlocked = true;
gameRunning = false; gameRunning = false;
onGameWin(elapsedSec); onGameWin(elapsedSec);
@@ -240,20 +300,20 @@
// Stone pillars // Stone pillars
ctx.fillStyle = 'rgba(25, 22, 36, 0.7)'; ctx.fillStyle = 'rgba(25, 22, 36, 0.7)';
for (let x = 120; x < width - 100; x += 180) { for (let x = 110; x < width - 90; x += 160) {
ctx.fillRect(x, 0, 35, height); ctx.fillRect(x, 0, 30, height);
ctx.strokeStyle = '#352e47'; ctx.strokeStyle = '#352e47';
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.strokeRect(x, 0, 35, height); ctx.strokeRect(x, 0, 30, height);
} }
// Vaulted arches at top // Vaulted arches at top
ctx.strokeStyle = '#2d273d'; ctx.strokeStyle = '#2d273d';
ctx.lineWidth = 4; ctx.lineWidth = 3;
ctx.beginPath(); ctx.beginPath();
for (let x = 0; x < width; x += 90) { for (let x = 0; x < width; x += 80) {
ctx.moveTo(x, 0); ctx.moveTo(x, 0);
ctx.quadraticCurveTo(x + 45, 60, x + 90, 0); ctx.quadraticCurveTo(x + 40, 50, x + 80, 0);
} }
ctx.stroke(); ctx.stroke();
@@ -285,11 +345,19 @@
}); });
ctx.shadowBlur = 0; 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 // Decoy keys
keys.forEach(k => { keys.forEach(k => {
ctx.save(); ctx.save();
ctx.translate(k.x, k.y); ctx.translate(k.x, k.y);
// wings
const wingY = Math.sin(k.wingPhase) * (k.size * 0.7); 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.fillStyle = k.gold ? 'rgba(255, 215, 0, 0.7)' : 'rgba(200, 220, 255, 0.65)';
ctx.beginPath(); ctx.beginPath();
@@ -312,7 +380,7 @@
ctx.translate(masterKey.x, masterKey.y); ctx.translate(masterKey.x, masterKey.y);
const mWingY = Math.sin(masterKey.wingPhase) * (masterKey.size * 0.8); const mWingY = Math.sin(masterKey.wingPhase) * (masterKey.size * 0.8);
ctx.shadowColor = '#00ffff'; ctx.shadowColor = '#00ffff';
ctx.shadowBlur = masterKey.caught ? 5 : 12; ctx.shadowBlur = masterKey.caught ? 6 : 14;
// Right intact wing // Right intact wing
ctx.fillStyle = 'rgba(180, 225, 255, 0.85)'; 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.ellipse(masterKey.size * 0.7, mWingY, masterKey.size * 0.9, masterKey.size * 0.4, -0.3, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
// Left BENT wing (as in the book!) // Left BENT wing
ctx.fillStyle = 'rgba(140, 190, 230, 0.75)'; ctx.fillStyle = 'rgba(140, 190, 230, 0.75)';
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, 0); ctx.moveTo(0, 0);
ctx.lineTo(-masterKey.size * 0.5, mWingY * 0.6); 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.lineTo(-masterKey.size * 0.4, mWingY * 0.2);
ctx.closePath(); ctx.closePath();
ctx.fill(); ctx.fill();
@@ -366,7 +434,7 @@
// Wizard silhouette (Cloak & Hat) // Wizard silhouette (Cloak & Hat)
ctx.fillStyle = '#1c172a'; ctx.fillStyle = '#1c172a';
ctx.beginPath(); ctx.beginPath();
ctx.arc(5, -12, 7, 0, Math.PI * 2); // head ctx.arc(5, -12, 7, 0, Math.PI * 2);
ctx.fill(); ctx.fill();
// Hat // Hat
ctx.fillStyle = '#120e1e'; ctx.fillStyle = '#120e1e';
@@ -377,7 +445,7 @@
ctx.closePath(); ctx.closePath();
ctx.fill(); ctx.fill();
// Cloak fluttering // Cloak fluttering
ctx.fillStyle = '#7a1f28'; // Gryffindor red cloak ctx.fillStyle = '#7a1f28';
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, -6); ctx.moveTo(0, -6);
ctx.lineTo(-20, -10 + Math.sin(Date.now() * 0.01) * 3); 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> 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>Flugzeit:</strong> ${timeTaken} Sekunden<br>
<strong>Punkte:</strong> ${score}<br><br> <strong>Punkte:</strong> ${score}<br><br>
Die Tür schwingt knarrend auf... Vor dir lodern violette und schwarze Flammen in Snapes Prüfung!`; Die Tür schwingt knarrend auf... Vor dir liegt McGonagalls gigantisches Zauberschach!`;
modalBtn.innerText = "Weiter zur Flammenkammer 🧪"; modalBtn.innerText = "Weiter zu McGonagalls Zauberschach ♟️";
modalBtn.onclick = () => { window.location.href = '/bottles'; }; modalBtn.onclick = () => { window.location.href = '/chess'; };
modalBackdrop.classList.add('show'); 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>🧹 Besenflug:</span>
<span>{% if state.broom_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %}</span> <span>{% if state.broom_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %}</span>
</div> </div>
<div class="stat-row">
<span>♟️ Zauberschach:</span>
<span>{% if state.chess_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %}</span>
</div>
<div class="stat-row"> <div class="stat-row">
<span>🧪 Snapes Flammen:</span> <span>🧪 Snapes Flammen:</span>
<span>{% if state.bottles_solved %}✅ Gelöst{% else %}⏳ Offen{% endif %}</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> <small>{% if state.broom_solved %}✅ Gemeistert{% else %}🗝️ Geflügelte Schlüssel jagen{% endif %}</small>
</div> </div>
</a> </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 %}"> <a href="/bottles" class="room-nav-item {% if request.url.path == '/bottles' %}current{% endif %}">
<span class="room-icon">🧪</span> <span class="room-icon">🧪</span>
<div class="room-info"> <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> <small>{% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Die Flammenkammer{% endif %}</small>
</div> </div>
</a> </a>
<a href="/sliding" class="room-nav-item {% if request.url.path == '/sliding' %}current{% endif %}"> <a href="/sliding" class="room-nav-item {% if request.url.path == '/sliding' %}current{% endif %}">
<span class="room-icon">🧩</span> <span class="room-icon">🧩</span>
<div class="room-info"> <div class="room-info">
+25 -10
View File
@@ -16,33 +16,48 @@
<div class="game-hud"> <div class="game-hud">
<div>⏱️ Zeit: <strong id="timerVal">0s</strong></div> <div>⏱️ Zeit: <strong id="timerVal">0s</strong></div>
<div>✨ Punkte: <strong id="scoreVal">0</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> </div>
<canvas id="broomCanvas"></canvas> <canvas id="broomCanvas"></canvas>
</div> </div>
<!-- Mobile Touch Controls --> <!-- Mobile Flight Cockpit & D-Pad -->
<div class="mobile-controls"> <div class="mobile-flight-cockpit">
<button id="mBtnUp" class="btn-control" style="font-size: 1.2rem; padding: 0.8rem 1.4rem;">▲ Hoch</button> <div class="dpad-box">
<button id="mBtnBoost" class="btn-control" style="font-size: 1.2rem; padding: 0.8rem 1.4rem; background: var(--gold); color: #000;">⚡ Turbo</button> <button type="button" id="mBtnUp" class="dpad-btn dpad-up" aria-label="Hoch">▲</button>
<button id="mBtnDown" class="btn-control" style="font-size: 1.2rem; padding: 0.8rem 1.4rem;">▼ Runter</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>
<div class="dungeon-card" style="margin-top: 1.5rem;"> <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;"> <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>
<p style="text-align: center; color: var(--text-dim); font-size: 0.95rem;"> <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> </p>
</div> </div>
<div style="text-align: center; margin-top: 1rem;"> <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> </div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="/static/js/broom.js?v=1.0"></script> <script src="/static/js/broom.js?v=2.0"></script>
{% endblock %} {% 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> <a href="/broom" class="btn-magical">Auf den Besen schwingen 🧹</a>
</div> </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"> <div class="chamber-card">
<span class="chamber-badge {% if state.bottles_solved %}solved{% endif %}"> <span class="chamber-badge {% if state.bottles_solved %}solved{% endif %}">
{% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Bereit{% endif %} {% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Bereit{% endif %}
</span> </span>
<div class="chamber-icon">🧪🔥</div> <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"> <p class="chamber-desc">
Schwarze Flammen versperren den Weg zum Stein, violette Flammen den Rückweg. Schwarze Flammen versperren den Weg zum Stein, violette Flammen den Rückweg.
Auf dem Tisch stehen 7 Flaschen und Snapes unerbittliches Logik-Gedicht. Auf dem Tisch stehen 7 Flaschen und Snapes unerbittliches Logik-Gedicht.
@@ -50,6 +64,7 @@
<a href="/bottles" class="btn-magical">Flammenkammer betreten 🧪</a> <a href="/bottles" class="btn-magical">Flammenkammer betreten 🧪</a>
</div> </div>
<!-- Bonus: Alchemy Sliding Puzzle --> <!-- Bonus: Alchemy Sliding Puzzle -->
<div class="chamber-card"> <div class="chamber-card">
<span class="chamber-badge {% if state.sliding_solved %}solved{% endif %}"> <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 res.status_code == 200
assert "Der Besenflug" in res.text 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") res = client.get("/bottles")
assert res.status_code == 200 assert res.status_code == 200
assert "Snapes Flaschenrätsel" in res.text assert "Snapes Flaschenrätsel" in res.text
@@ -50,6 +54,14 @@ def test_endpoints():
assert data["success"] is True assert data["success"] is True
assert "silver_key" in data["state"]["inventory"] 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) # Test API Bottles drink: Ahead / Black fire (Bottle 3 - Dwarf, adds philosophers_stone)
res = client.post("/api/bottles/drink", json={"bottle": 3, "gate": "ahead"}) res = client.post("/api/bottles/drink", json={"bottle": 3, "gate": "ahead"})
assert res.status_code == 200 assert res.status_code == 200
@@ -63,7 +75,7 @@ def test_endpoints():
assert res.status_code == 200 assert res.status_code == 200
assert res.json()["success"] is True assert res.json()["success"] is True
print("ALL INVENTORY & GAMEPLAY TESTS PASSED! 🎒✨") print("ALL TESTS (INCLUDING CHESS & MOBILE FLIGHT) PASSED! ♟️🧹✨")
if __name__ == "__main__": if __name__ == "__main__":
test_endpoints() test_endpoints()