diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc
index 91b5932..715350d 100644
Binary files a/app/__pycache__/main.cpython-313.pyc and b/app/__pycache__/main.cpython-313.pyc differ
diff --git a/app/main.py b/app/main.py
index 8cecfbf..ba3bbeb 100755
--- a/app/main.py
+++ b/app/main.py
@@ -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!"
diff --git a/app/static/css/escape.css b/app/static/css/escape.css
index 7f87aa9..c71a7ce 100755
--- a/app/static/css/escape.css
+++ b/app/static/css/escape.css
@@ -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;
+ }
+}
+
diff --git a/app/static/js/broom.js b/app/static/js/broom.js
index 9be927b..ee1d37f 100755
--- a/app/static/js/broom.js
+++ b/app/static/js/broom.js
@@ -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 = "✨ Schlüssel gefangen & im Beutel! Fliege zur Holztür rechts und öffne das Schloss!";
+ statusMsg.innerHTML = "✨ Schlüssel gefangen & im Beutel! 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!
Flugzeit: ${timeTaken} Sekunden
Punkte: ${score}
- 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');
}
})();
diff --git a/app/static/js/chess.js b/app/static/js/chess.js
new file mode 100755
index 0000000..51574e3
--- /dev/null
+++ b/app/static/js/chess.js
@@ -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 = "🧙♂️ Du führst Schwarz (Rons Springer & Hermines Figuren)! Finde den vernichtenden Zug gegen den weißen König!";
+ statusMsg.style.color = "var(--gold)";
+ }
+ if (moveHistoryEl) moveHistoryEl.innerHTML = "
- 🎮 Steuerung: Pfeiltasten / WASD / Mausbewegung zum Fliegen · Leertaste / Klick für Besen-Turbo! + 🎮 Steuerung: Pfeiltasten / WASD / Touch-Wischen zum Fliegen · Leertaste / Turbo für Besen-Beschleunigung!
- 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!
+ "Wir müssen uns den Weg quer durch das Zimmer freispielen!", rief Ron.
+ Hinter den weißen Riesenfiguren liegt die schwere Eichentür zur Flammenkammer.
+ Übernimm die Kontrolle über die schwarzen Steine und setze den weißen König schachmatt!
+
+ Der weiße König steht in der Ecke auf h1, eingeklemmt von seinen eigenen Bauern auf g2 und h2. +
++ 🎯 Möglichkeit 1: Führe mit dem schwarzen Springer auf g4 ein verheerendes Schachgebot auf f2 aus (♞f2+) – Rons klassisches Springer-Opfer! +
++ 🎯 Möglichkeit 2: Schlage mit der schwarzen Dame auf h4 direkt den Bauern auf h2 (♛xh2#) – augenblickliches Schachmatt mit Springer-Deckung! +
++ Das gigantische, lebendige Zauberschach. Übernimm Rons Rolle als schwarzer Springer, + überliste die weiße Königin und setze den feindlichen König schachmatt! +
+ Schachbrett betreten ♟️ +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 @@ Flammenkammer betreten 🧪