diff --git a/.gitignore b/.gitignore index d0ee3b1..9b5f630 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -venv/ __pycache__/ *.pyc +venv/ diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc index 715350d..f6fc3f0 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 ba3bbeb..e7f3ba4 100755 --- a/app/main.py +++ b/app/main.py @@ -11,7 +11,7 @@ from dotenv import load_dotenv load_dotenv() -app = FastAPI(title="Hogwarts Escape Adventure - Die Kammern des Steins der Weisen") +app = FastAPI(title="Hogwarts Escape Adventure - Die Prüfungen der Zauberei") SECRET_KEY = os.getenv("SECRET_KEY", "alohomora_bootle_potion_secret_2026") app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, session_cookie="escape_session") @@ -36,7 +36,7 @@ ALL_ITEMS = { "id": "wand", "name": "Zauberstab", "icon": "🪄", - "desc": "Stechpalme und Phönixfeder, 11 Zoll. Liegt warm in deiner Hand.", + "desc": "Stechpalme und Phönixfeder, 11 Zoll. Liegt warm und vibrationsbereit in deiner Hand.", "action": "lumos", "action_label": "Lumos / Licht 💡" }, @@ -44,10 +44,18 @@ ALL_ITEMS = { "id": "broom", "name": "Nimbus 2000", "icon": "🧹", - "desc": "Ein pfeilschneller Rennbesen. Perfekt balanciert für Wendemanöver im Schlüsselraum.", + "desc": "Ein pfeilschneller Rennbesen. Perfekt für den rasanten Flug durch schwebende Ringe!", "action": "broom_polish", "action_label": "Besen polieren ✨" }, + "duel_badge": { + "id": "duel_badge", + "name": "Duellanten-Abzeichen", + "icon": "⚡", + "desc": "Ein goldenes Abzeichen für meisterhafte Zauberstab-Gesten im Duellier-Club von Hogwarts.", + "action": "spark_wand", + "action_label": "Funkenregen sprühen ⚡" + }, "snape_scroll": { "id": "snape_scroll", "name": "Snapes Logik-Rolle", @@ -56,22 +64,6 @@ ALL_ITEMS = { "action": "read_scroll", "action_label": "Rätsel-Verse lesen 📖" }, - "silver_key": { - "id": "silver_key", - "name": "Geknickter Silberschlüssel", - "icon": "🗝️", - "desc": "Der schwere antike Schlüssel aus der Schlüsselkammer. Sein geknickter Flügel verrät ihn.", - "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", @@ -98,7 +90,7 @@ def get_session_state(request: Request) -> dict: "broom_solved": False, "broom_score": 0, "broom_time": 0, - "chess_solved": False, + "duel_solved": False, "bottles_solved": False, "sliding_solved": False, "started_at": datetime.datetime.utcnow().isoformat(), @@ -109,16 +101,14 @@ def get_session_state(request: Request) -> dict: 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("duel_solved"): + state["inventory"].append("duel_badge") 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 + if "duel_solved" not in state: + state["duel_solved"] = False request.session["escape"] = state return request.session["escape"] @@ -159,18 +149,23 @@ 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): +@app.get("/duel", response_class=HTMLResponse) +async def page_duel(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", + name="duel.html", context=get_template_context(request, state, lang) ) +@app.get("/chess") +async def redirect_chess(): + # Graceful redirect for old links + return RedirectResponse(url="/duel", status_code=302) + @app.get("/bottles", response_class=HTMLResponse) async def page_bottles(request: Request): state = get_session_state(request) @@ -223,7 +218,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), + "duel_solved": state.get("duel_solved", False), "bottles_solved": state.get("bottles_solved", False), "sliding_solved": state.get("sliding_solved", False) }) @@ -251,7 +246,7 @@ async def api_reset_state(request: Request): "broom_solved": False, "broom_score": 0, "broom_time": 0, - "chess_solved": False, + "duel_solved": False, "bottles_solved": False, "sliding_solved": False, "started_at": datetime.datetime.utcnow().isoformat(), @@ -270,32 +265,26 @@ 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 - if "inventory" not in state: - state["inventory"] = ["wand", "broom", "snape_scroll"] - if "silver_key" not in state["inventory"]: - state["inventory"].append("silver_key") request.session["escape"] = state return JSONResponse({ "success": True, - "message": "Der Schlüssel passt ins Schloss! Die Tür zu McGonagalls Zauberschach steht offen!", - "new_item": ALL_ITEMS["silver_key"], + "message": "Parcours gemeistert! Die Tür zum Zauberspruch-Duell steht offen!", "state": state }) -@app.post("/api/chess/complete") -async def api_chess_complete(request: Request): +@app.post("/api/duel/complete") +async def api_duel_complete(request: Request): state = get_session_state(request) - state["chess_solved"] = True + state["duel_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") + if "duel_badge" not in state["inventory"]: + state["inventory"].append("duel_badge") 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"], + "message": "Zauberspruch-Duell gewonnen! Die Pforte zur Flammenkammer steht weit offen!", + "new_item": ALL_ITEMS["duel_badge"], "state": state }) @@ -318,7 +307,7 @@ async def api_bottles_drink(request: Request): if content == "ahead": if target_gate == "ahead": state["bottles_solved"] = True - if state.get("broom_solved"): + if state.get("broom_solved") or state.get("duel_solved"): state["completed_at"] = datetime.datetime.utcnow().isoformat() if "inventory" not in state: state["inventory"] = ["wand", "broom", "snape_scroll"] @@ -349,7 +338,7 @@ async def api_bottles_drink(request: Request): "result": "success_back", "content": content, "message": msg_de if lang == "de" else msg_en, - "next_url": "/chess" + "next_url": "/duel" }) 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/js/broom.js b/app/static/js/broom.js index ee1d37f..a9bedee 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) - Mobile & Touch Optimized +// Broomstick Flight: Golden Ring Course (Quidditch-Flugtraining) - Mobile & Touch Optimized (function() { const canvas = document.getElementById('broomCanvas'); if (!canvas) return; @@ -7,88 +7,90 @@ // UI elements const scoreVal = document.getElementById('scoreVal'); const timerVal = document.getElementById('timerVal'); + const ringsVal = document.getElementById('ringsVal'); + const comboVal = document.getElementById('comboVal'); const statusMsg = document.getElementById('statusMsg'); const modalBackdrop = document.getElementById('modalBackdrop'); const modalTitle = document.getElementById('modalTitle'); const modalText = document.getElementById('modalText'); const modalBtn = document.getElementById('modalBtn'); - let width = 800, height = 480; + let width = 800, height = 460; function resize() { 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))); + height = Math.round(Math.min(500, Math.max(280, width * 0.56))); canvas.width = width; canvas.height = height; canvas.style.height = height + 'px'; - - if (door) { - door.x = width - 55; - door.y = height / 2 - 45; - } } - // Target Door - const door = { - x: width - 55, - y: height / 2 - 45, - w: 42, - h: 90 - }; - - // Game State + // Game Configuration + const TOTAL_RINGS_NEEDED = 12; let gameRunning = true; let startTime = Date.now(); let score = 0; - let keyCaught = false; - let doorUnlocked = false; + let ringsCollected = 0; + let combo = 0; // Player (Broomstick) const player = { - x: 80, - y: 200, + x: 90, + y: 220, + targetY: 220, + targetX: 90, vx: 0, vy: 0, - speed: 6.0, + speed: 6.5, boost: 1.0, angle: 0, trail: [] }; - // Keys Swarm - const keys = []; - const NUM_KEYS = 22; + // Flying Golden Rings + const rings = []; + let lastRingSpawn = Date.now(); - // Master Key (Damaged wing silver key) - const masterKey = { - x: 600, - y: 200, - vx: -2.5, - vy: 1.5, - wingPhase: 0, - caught: false, - size: 17 - }; - - // Initialize Canvas Size - window.addEventListener('resize', resize); - resize(); - - // Spawn decoy keys - for (let i = 0; i < NUM_KEYS; i++) { - keys.push({ - x: Math.random() * (width - 150) + 100, - y: Math.random() * (height - 60) + 30, - vx: (Math.random() - 0.5) * 4, - vy: (Math.random() - 0.5) * 3, - size: Math.random() * 6 + 8, - wingPhase: Math.random() * Math.PI * 2, - wingSpeed: Math.random() * 0.3 + 0.2, - gold: Math.random() > 0.4 + function spawnRing() { + // Rings glide from right to left + const ringY = Math.random() * (height - 120) + 60; + const isGoldenBonus = Math.random() > 0.75; + rings.push({ + x: width + 40, + y: ringY, + radius: isGoldenBonus ? 28 : 24, + speed: (Math.random() * 1.5 + 3.2), + golden: isGoldenBonus, + collected: false, + scale: 1.0, + alpha: 1.0, + pulse: 0 }); } - // Keyboard Controls + // Sparkles & Confetti particles + const particles = []; + function spawnSparkles(x, y, color) { + for (let i = 0; i < 18; i++) { + const angle = Math.random() * Math.PI * 2; + const spd = Math.random() * 5 + 2; + particles.push({ + x: x, + y: y, + vx: Math.cos(angle) * spd, + vy: Math.sin(angle) * spd, + color: color || '#ffd700', + size: Math.random() * 4 + 2, + alpha: 1.0 + }); + } + } + + // Initialize Canvas + window.addEventListener('resize', resize); + resize(); + + // Controls: Keyboard const keysDown = {}; window.addEventListener('keydown', e => { keysDown[e.key.toLowerCase()] = true; @@ -102,8 +104,8 @@ if (e.key === ' ' || e.key === 'Shift') player.boost = 1.0; }); - // Touch & Pointer Steering - let pointerTarget = null; + // Touch & Pointer Direct Steering (Smooth Follow-Touch) + let touchSteering = false; function getCanvasCoords(clientX, clientY) { const rect = canvas.getBoundingClientRect(); @@ -113,11 +115,13 @@ }; } - // Direct Touch Listeners to prevent browser scrolling canvas.addEventListener('touchstart', e => { e.preventDefault(); + touchSteering = true; if (e.touches.length > 0) { - pointerTarget = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY); + const coords = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY); + player.targetX = coords.x; + player.targetY = coords.y; player.boost = 1.4; if (window.soundFX) window.soundFX.playWhoosh(); } @@ -126,31 +130,38 @@ canvas.addEventListener('touchmove', e => { e.preventDefault(); if (e.touches.length > 0) { - pointerTarget = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY); + const coords = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY); + player.targetX = coords.x; + player.targetY = coords.y; } }, { passive: false }); canvas.addEventListener('touchend', e => { e.preventDefault(); - pointerTarget = null; + touchSteering = false; player.boost = 1.0; }, { passive: false }); - // Pointer events for mouse + // Pointer events for Desktop Mouse canvas.addEventListener('pointerdown', e => { if (e.pointerType === 'mouse') { - pointerTarget = getCanvasCoords(e.clientX, e.clientY); + touchSteering = true; + const coords = getCanvasCoords(e.clientX, e.clientY); + player.targetX = coords.x; + player.targetY = coords.y; 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); + if (touchSteering) { + const coords = getCanvasCoords(e.clientX, e.clientY); + player.targetX = coords.x; + player.targetY = coords.y; } }); canvas.addEventListener('pointerup', () => { - pointerTarget = null; + touchSteering = false; player.boost = 1.0; }); @@ -190,103 +201,115 @@ btnBoost.addEventListener('pointerleave', endBoost); } + // Pre-spawn 3 rings + rings.push({ x: width + 50, y: height * 0.4, radius: 25, speed: 3.5, golden: false, collected: false, scale: 1, alpha: 1, pulse: 0 }); + rings.push({ x: width + 280, y: height * 0.65, radius: 25, speed: 3.6, golden: true, collected: false, scale: 1, alpha: 1, pulse: 0 }); + rings.push({ x: width + 520, y: height * 0.3, radius: 25, speed: 3.7, golden: false, collected: false, scale: 1, alpha: 1, pulse: 0 }); + function update() { if (!gameRunning) return; const elapsedSec = Math.floor((Date.now() - startTime) / 1000); if (timerVal) timerVal.innerText = elapsedSec + 's'; - // Keyboard & D-Pad Movement + // Spawn new rings continuously + if (Date.now() - lastRingSpawn > 1400) { + spawnRing(); + lastRingSpawn = Date.now(); + } + + // Steer player 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 (Touch / Mouse direct following) - if (pointerTarget) { - const dx = pointerTarget.x - player.x; - const dy = pointerTarget.y - player.y; + if (touchSteering) { + const dx = player.targetX - player.x; + const dy = player.targetY - player.y; const dist = Math.hypot(dx, dy); - if (dist > 12) { + if (dist > 8) { ax = dx / dist; ay = dy / dist; } } const curSpeed = player.speed * player.boost; - player.vx = player.vx * 0.82 + ax * curSpeed * 0.22; - player.vy = player.vy * 0.82 + ay * curSpeed * 0.22; + player.vx = player.vx * 0.82 + ax * curSpeed * 0.24; + player.vy = player.vy * 0.82 + ay * curSpeed * 0.24; player.x += player.vx; player.y += player.vy; - // Bounds - player.x = Math.max(30, Math.min(width - 65, player.x)); - player.y = Math.max(30, Math.min(height - 30, player.y)); + // Keep inside bounds + player.x = Math.max(35, Math.min(width - 55, player.x)); + player.y = Math.max(30, Math.min(height - 35, player.y)); - player.angle = Math.atan2(player.vy, player.vx + 2) * 0.55; + player.angle = Math.atan2(player.vy, player.vx + 3) * 0.5; - // Particles trail - 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(); + // Particle trail from broom tail + player.trail.push({ x: player.x - 24, 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.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 < 40 || k.x > width - 90) k.vx *= -1; - if (k.y < 30 || k.y > height - 30) k.vy *= -1; - }); + // Update rings + for (let i = rings.length - 1; i >= 0; i--) { + const r = rings[i]; + r.x -= r.speed * (player.boost > 1.2 ? 1.4 : 1.0); + r.pulse += 0.08; - // Update Master Key - if (!masterKey.caught) { - masterKey.wingPhase += 0.35; - const dToPlayer = Math.hypot(player.x - masterKey.x, player.y - masterKey.y); - if (dToPlayer < 150) { - masterKey.vx += (masterKey.x - player.x) * 0.045; - masterKey.vy += (masterKey.y - player.y) * 0.045; + if (!r.collected) { + // Check if player flew through ring + const dist = Math.hypot(player.x - r.x, player.y - r.y); + if (dist < r.radius + 18) { + r.collected = true; + ringsCollected++; + combo++; + const ringPts = (r.golden ? 300 : 150) * Math.min(4, combo); + score += ringPts; + + if (scoreVal) scoreVal.innerText = score; + if (ringsVal) ringsVal.innerText = `${ringsCollected}/${TOTAL_RINGS_NEEDED}`; + if (comboVal) comboVal.innerText = combo > 1 ? `x${combo} Combo!` : ''; + + if (window.soundFX) window.soundFX.playKeyChime(); + spawnSparkles(r.x, r.y, r.golden ? '#ffd700' : '#00ffff'); + + // Check Course Win + if (ringsCollected >= TOTAL_RINGS_NEEDED) { + gameRunning = false; + onCourseWin(elapsedSec); + } + } } else { - masterKey.vx += (Math.random() - 0.5) * 0.9; - masterKey.vy += (Math.random() - 0.5) * 0.9; - } - 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(90, Math.min(width - 100, masterKey.x)); - masterKey.y = Math.max(40, Math.min(height - 40, masterKey.y)); - - // Catch check (generous hit box for mobile thumbs: 42px) - if (dToPlayer < 42) { - masterKey.caught = true; - keyCaught = true; - score += 500; - if (scoreVal) scoreVal.innerText = score; - if (window.soundFX) window.soundFX.playKeyChime(); - if (window.notifyNewItem) { - window.notifyNewItem({ icon: "🗝️", name: "Geknickter Silberschlüssel" }); - } - if (statusMsg) { - statusMsg.innerHTML = "✨ Schlüssel gefangen & im Beutel! Fliege jetzt zur schweren Holztür rechts!"; - statusMsg.style.color = "#2ecc71"; + // Animation after collection + r.scale += 0.08; + r.alpha -= 0.08; + if (r.alpha <= 0) { + rings.splice(i, 1); + continue; } } - } else { - // Master key follows player - masterKey.x = player.x + 10; - masterKey.y = player.y - 15; - // 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 < 60 && !doorUnlocked) { - doorUnlocked = true; - gameRunning = false; - onGameWin(elapsedSec); + // Remove rings that flew past screen + if (r.x < -60) { + if (!r.collected) { + combo = 0; // Combo reset on miss + if (comboVal) comboVal.innerText = ''; + } + rings.splice(i, 1); + } + } + + // Update sparkles + for (let i = particles.length - 1; i >= 0; i--) { + const p = particles[i]; + p.x += p.vx; + p.y += p.vy; + p.alpha *= 0.92; + if (p.alpha <= 0.02) { + particles.splice(i, 1); } } } @@ -294,121 +317,100 @@ function render() { ctx.clearRect(0, 0, width, height); - // Dungeon stone background & arches - ctx.fillStyle = '#0f0e17'; + // Night sky gradient with Hogwarts towers silhouettes + const skyGrad = ctx.createLinearGradient(0, 0, 0, height); + skyGrad.addColorStop(0, '#0c0b17'); + skyGrad.addColorStop(1, '#1b142e'); + ctx.fillStyle = skyGrad; ctx.fillRect(0, 0, width, height); - // Stone pillars - ctx.fillStyle = 'rgba(25, 22, 36, 0.7)'; - 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, 30, height); + // Stars + ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; + for (let i = 0; i < 25; i++) { + const sx = (i * 37 + (Date.now() * 0.02)) % width; + const sy = (i * 53) % (height * 0.65); + ctx.fillRect(sx, sy, 1.5, 1.5); } - // Vaulted arches at top - ctx.strokeStyle = '#2d273d'; - ctx.lineWidth = 3; + // Hogwarts distant castle spires in background + ctx.fillStyle = 'rgba(15, 12, 25, 0.85)'; ctx.beginPath(); - for (let x = 0; x < width; x += 80) { - ctx.moveTo(x, 0); - ctx.quadraticCurveTo(x + 40, 50, x + 80, 0); - } - ctx.stroke(); - - // The Door - ctx.fillStyle = doorUnlocked ? '#1f6f3e' : '#4a2f16'; - ctx.fillRect(door.x, door.y, door.w, door.h); - ctx.strokeStyle = '#e5b948'; - ctx.lineWidth = 3; - ctx.strokeRect(door.x, door.y, door.w, door.h); - - // Keyhole - ctx.fillStyle = keyCaught ? '#ffd700' : '#111'; - ctx.beginPath(); - ctx.arc(door.x + door.w / 2, door.y + door.h / 2 - 6, 6, 0, Math.PI * 2); + ctx.moveTo(0, height); + ctx.lineTo(80, height - 90); + ctx.lineTo(120, height - 160); + ctx.lineTo(160, height - 90); + ctx.lineTo(240, height - 120); + ctx.lineTo(310, height - 190); + ctx.lineTo(380, height - 100); + ctx.lineTo(460, height - 140); + ctx.lineTo(550, height - 80); + ctx.lineTo(650, height - 180); + ctx.lineTo(750, height - 110); + ctx.lineTo(width, height - 90); + ctx.lineTo(width, height); + ctx.closePath(); ctx.fill(); - ctx.fillRect(door.x + door.w / 2 - 3, door.y + door.h / 2 - 6, 6, 14); - if (keyCaught) { - ctx.shadowColor = '#ffd700'; - ctx.shadowBlur = 15; - } - - // Sparkle trail + // Sparkle trail behind broom player.trail.forEach(p => { ctx.fillStyle = `rgba(229, 185, 72, ${p.alpha})`; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); }); - ctx.shadowBlur = 0; - // Touch target indicator - if (pointerTarget && gameRunning) { - ctx.strokeStyle = 'rgba(229, 185, 72, 0.5)'; + // Touch steer indicator + if (touchSteering && gameRunning) { + ctx.strokeStyle = 'rgba(229, 185, 72, 0.45)'; ctx.lineWidth = 2; ctx.beginPath(); - ctx.arc(pointerTarget.x, pointerTarget.y, 20 + Math.sin(Date.now() * 0.01) * 4, 0, Math.PI * 2); + ctx.arc(player.targetX, player.targetY, 22 + Math.sin(Date.now() * 0.01) * 3, 0, Math.PI * 2); ctx.stroke(); } - // Decoy keys - keys.forEach(k => { + // Render Golden Rings + rings.forEach(r => { ctx.save(); - ctx.translate(k.x, k.y); - 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(); - ctx.ellipse(-k.size * 0.6, wingY, k.size * 0.8, k.size * 0.35, 0.3, 0, Math.PI * 2); - ctx.ellipse(k.size * 0.6, wingY, k.size * 0.8, k.size * 0.35, -0.3, 0, Math.PI * 2); - ctx.fill(); + ctx.translate(r.x, r.y); + ctx.scale(r.scale, r.scale); + ctx.globalAlpha = r.alpha; - // Key body - ctx.fillStyle = k.gold ? '#e5b948' : '#c0c5cc'; + // Outer golden glow + ctx.shadowColor = r.golden ? '#ffd700' : '#4cc9f0'; + ctx.shadowBlur = r.golden ? 18 : 12; + + // Ring Outer Circle + ctx.strokeStyle = r.golden ? '#ffd700' : '#7209b7'; + ctx.lineWidth = r.golden ? 5 : 4; ctx.beginPath(); - ctx.arc(0, 0, k.size * 0.35, 0, Math.PI * 2); - ctx.fill(); - ctx.fillRect(-1.5, 0, 3, k.size * 1.1); - ctx.fillRect(0, k.size * 0.7, k.size * 0.4, 2.5); + ctx.arc(0, 0, r.radius, 0, Math.PI * 2); + ctx.stroke(); + + // Inner Ring Highlights + ctx.strokeStyle = r.golden ? '#fff275' : '#4895ef'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(0, 0, r.radius - 3, 0, Math.PI * 2); + ctx.stroke(); + + // Ring Mount / Pole at bottom + ctx.fillStyle = '#6b5338'; + ctx.fillRect(-2, r.radius, 4, 30); + ctx.restore(); }); - - // Master Key (Damaged Wing) - ctx.save(); - ctx.translate(masterKey.x, masterKey.y); - const mWingY = Math.sin(masterKey.wingPhase) * (masterKey.size * 0.8); - ctx.shadowColor = '#00ffff'; - ctx.shadowBlur = masterKey.caught ? 6 : 14; - - // Right intact wing - ctx.fillStyle = 'rgba(180, 225, 255, 0.85)'; - ctx.beginPath(); - 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 - 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); - ctx.lineTo(-masterKey.size * 0.4, mWingY * 0.2); - ctx.closePath(); - ctx.fill(); - - // Heavy antique key body - ctx.fillStyle = '#e0e8f0'; - ctx.beginPath(); - ctx.arc(0, 0, masterKey.size * 0.4, 0, Math.PI * 2); - ctx.fill(); - ctx.fillRect(-2, 0, 4, masterKey.size * 1.3); - ctx.fillRect(0, masterKey.size * 0.8, masterKey.size * 0.5, 3); - ctx.fillRect(0, masterKey.size * 1.05, masterKey.size * 0.4, 3); - ctx.restore(); ctx.shadowBlur = 0; + // Render Sparkles + particles.forEach(p => { + ctx.fillStyle = p.color; + ctx.globalAlpha = p.alpha; + ctx.beginPath(); + ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); + ctx.fill(); + }); + ctx.globalAlpha = 1.0; + // Player (Broom & Wizard) ctx.save(); ctx.translate(player.x, player.y); @@ -444,11 +446,11 @@ ctx.lineTo(2, -26); ctx.closePath(); ctx.fill(); - // Cloak fluttering - ctx.fillStyle = '#7a1f28'; + // Gryffindor / House cloak fluttering + ctx.fillStyle = '#800e13'; ctx.beginPath(); ctx.moveTo(0, -6); - ctx.lineTo(-20, -10 + Math.sin(Date.now() * 0.01) * 3); + ctx.lineTo(-20, -10 + Math.sin(Date.now() * 0.012) * 3); ctx.lineTo(-24, 6); ctx.lineTo(10, 0); ctx.closePath(); @@ -457,9 +459,9 @@ // Wand tip spark ctx.fillStyle = '#ffff99'; ctx.shadowColor = '#ffff00'; - ctx.shadowBlur = 10; + ctx.shadowBlur = 12; ctx.beginPath(); - ctx.arc(24, -8, 3, 0, Math.PI * 2); + ctx.arc(24, -8, 3.5, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; @@ -473,25 +475,32 @@ } requestAnimationFrame(loop); - async function onGameWin(timeTaken) { + async function onCourseWin(elapsedSec) { if (window.soundFX) window.soundFX.playMagicSuccess(); try { - await fetch('/api/broom/complete', { + const res = await fetch('/api/broom/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ score: score, time_taken: timeTaken }) + body: JSON.stringify({ score: score, time_taken: elapsedSec }) }); + const data = await res.json(); + if (data.new_item && window.notifyNewItem) { + window.notifyNewItem(data.new_item); + } } catch (e) { console.error(e); } - modalTitle.innerText = "🏆 Raum 1 gemeistert!"; - 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 liegt McGonagalls gigantisches Zauberschach!`; - modalBtn.innerText = "Weiter zu McGonagalls Zauberschach ♟️"; - modalBtn.onclick = () => { window.location.href = '/chess'; }; + modalTitle.innerText = "🏆 Ring-Parcours Gemeistert!"; + modalText.innerHTML = ` + Mit atemberaubenden Manövern hast du alle ${TOTAL_RINGS_NEEDED} goldenen Ringe durchflogen!

+ Flugzeit: ${elapsedSec} Sekunden
+ Punkte: ${score}

+ Madam Hooch nickt anerkennend und überreicht dir die Flug-Auszeichnung!
+ Das Tor zur nächsten Kammer steht offen! + `; + modalBtn.innerText = "Weiter zum Zauberspruch-Duell 🪄"; + modalBtn.onclick = () => { window.location.href = '/duel'; }; modalBackdrop.classList.add('show'); } })(); diff --git a/app/static/js/duel.js b/app/static/js/duel.js new file mode 100755 index 0000000..0c6ccec --- /dev/null +++ b/app/static/js/duel.js @@ -0,0 +1,454 @@ +// Wand Spell Tracing & Dueling Mini-Game (Das Zauberspruch-Duell) +(function() { + const canvas = document.getElementById('spellCanvas'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + + const spellNameEl = document.getElementById('spellName'); + const spellDescEl = document.getElementById('spellDesc'); + const spellProgressEl = document.getElementById('spellProgress'); + const dummyHpEl = document.getElementById('dummyHp'); + const modalBackdrop = document.getElementById('modalBackdrop'); + const modalTitle = document.getElementById('modalTitle'); + const modalText = document.getElementById('modalText'); + const modalBtn = document.getElementById('modalBtn'); + + let width = 700, height = 440; + function resize() { + const rect = canvas.getBoundingClientRect(); + width = Math.round(rect.width || canvas.parentElement.clientWidth || 700); + height = Math.round(Math.min(480, Math.max(280, width * 0.62))); + canvas.width = width; + canvas.height = height; + canvas.style.height = height + 'px'; + setupSpellNodes(); + } + + // 3 Sequential Spells with distinct gestures + const SPELLS = [ + { + id: 'flipendo', + name: 'Flipendo ⚡', + incantation: '"Flipendo!" (Ruck-Schockzauber)', + desc: 'Verbinde die 4 Runensterne im Zickzack-Muster, um den Schockzauber zu entfesseln!', + color: '#00f2fe', + beamColor: '#4facfe', + // Relative coordinates (0 to 1) for responsive scaling + points: [ + { x: 0.22, y: 0.28 }, + { x: 0.52, y: 0.28 }, + { x: 0.25, y: 0.72 }, + { x: 0.55, y: 0.72 } + ] + }, + { + id: 'incendio', + name: 'Incendio 🔥', + incantation: '"Incendio!" (Feuer-Zauber)', + desc: 'Zeichne das lodernde Flammendreieck, um die Zielattrappe in Brand zu setzen!', + color: '#ff4500', + beamColor: '#ff8c00', + points: [ + { x: 0.38, y: 0.22 }, + { x: 0.55, y: 0.74 }, + { x: 0.22, y: 0.74 }, + { x: 0.38, y: 0.22 } + ] + }, + { + id: 'alohomora', + name: 'Alohomora 🗝️', + incantation: '"Alohomora!" (Entriegelungszauber)', + desc: 'Zeichne die magische Schlüsselloch-Kurve, um das verzauberte Siegel zu sprengen!', + color: '#ffd700', + beamColor: '#fff275', + points: [ + { x: 0.38, y: 0.25 }, + { x: 0.48, y: 0.38 }, + { x: 0.42, y: 0.52 }, + { x: 0.42, y: 0.78 } + ] + } + ]; + + let currentSpellIdx = 0; + let spellNodes = []; + let nextNodeIdx = 0; + let isDrawing = false; + let userPath = []; + let wandParticles = []; + let blastParticles = []; + let dummyShake = 0; + let spellCastAnim = 0; + let gameWon = false; + + function setupSpellNodes() { + const spell = SPELLS[currentSpellIdx]; + if (!spell) return; + spellNodes = spell.points.map((pt, idx) => ({ + x: Math.round(pt.x * width), + y: Math.round(pt.y * height), + radius: 20, + hit: idx < nextNodeIdx + })); + } + + window.addEventListener('resize', resize); + resize(); + + function updateSpellUI() { + const spell = SPELLS[currentSpellIdx]; + if (!spell) return; + if (spellNameEl) spellNameEl.innerText = spell.name; + if (spellDescEl) spellDescEl.innerText = spell.desc; + if (spellProgressEl) spellProgressEl.innerText = `Zauberspruch ${currentSpellIdx + 1} von ${SPELLS.length}`; + if (dummyHpEl) dummyHpEl.innerText = `${SPELLS.length - currentSpellIdx} Zauber übrig`; + setupSpellNodes(); + } + updateSpellUI(); + + // Touch & Pointer Coordinates + function getCanvasCoords(clientX, clientY) { + const rect = canvas.getBoundingClientRect(); + return { + x: (clientX - rect.left) * (canvas.width / rect.width), + y: (clientY - rect.top) * (canvas.height / rect.height) + }; + } + + function addWandSpark(x, y, color) { + for (let i = 0; i < 4; i++) { + wandParticles.push({ + x: x, + y: y, + vx: (Math.random() - 0.5) * 3, + vy: (Math.random() - 0.5) * 3, + color: color || '#fff', + size: Math.random() * 4 + 2, + alpha: 1.0 + }); + } + } + + function handleInputStart(x, y) { + if (gameWon) return; + isDrawing = true; + userPath = [{ x, y }]; + checkNodeHit(x, y); + } + + function handleInputMove(x, y) { + if (!isDrawing || gameWon) return; + userPath.push({ x, y }); + addWandSpark(x, y, SPELLS[currentSpellIdx].color); + checkNodeHit(x, y); + } + + function handleInputEnd() { + isDrawing = false; + // If not all nodes hit, reset progress for this spell + if (nextNodeIdx < spellNodes.length) { + nextNodeIdx = 0; + userPath = []; + setupSpellNodes(); + } + } + + function checkNodeHit(x, y) { + if (nextNodeIdx >= spellNodes.length) return; + const targetNode = spellNodes[nextNodeIdx]; + const dist = Math.hypot(x - targetNode.x, y - targetNode.y); + + // Tolerant hit radius for easy finger play: 36px + if (dist < 36) { + targetNode.hit = true; + nextNodeIdx++; + if (window.soundFX) window.soundFX.playKeyChime(); + addWandSpark(targetNode.x, targetNode.y, '#ffd700'); + + // All nodes connected for this spell! + if (nextNodeIdx >= spellNodes.length) { + castSpellSuccess(); + } + } + } + + // Touch events with touch-action none + canvas.addEventListener('touchstart', e => { + e.preventDefault(); + if (e.touches.length > 0) { + const p = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY); + handleInputStart(p.x, p.y); + } + }, { passive: false }); + + canvas.addEventListener('touchmove', e => { + e.preventDefault(); + if (e.touches.length > 0) { + const p = getCanvasCoords(e.touches[0].clientX, e.touches[0].clientY); + handleInputMove(p.x, p.y); + } + }, { passive: false }); + + canvas.addEventListener('touchend', e => { + e.preventDefault(); + handleInputEnd(); + }, { passive: false }); + + // Pointer events for desktop + canvas.addEventListener('pointerdown', e => { + if (e.pointerType === 'mouse') { + const p = getCanvasCoords(e.clientX, e.clientY); + handleInputStart(p.x, p.y); + } + }); + + canvas.addEventListener('pointermove', e => { + if (e.pointerType === 'mouse' && isDrawing) { + const p = getCanvasCoords(e.clientX, e.clientY); + handleInputMove(p.x, p.y); + } + }); + + canvas.addEventListener('pointerup', () => { + handleInputEnd(); + }); + + function castSpellSuccess() { + const spell = SPELLS[currentSpellIdx]; + spellCastAnim = 1.0; + dummyShake = 18; + if (window.soundFX) window.soundFX.playMagicSuccess(); + + // Explosion on target dummy + const dummyX = width * 0.78; + const dummyY = height * 0.5; + for (let i = 0; i < 40; i++) { + const angle = Math.random() * Math.PI * 2; + const spd = Math.random() * 8 + 3; + blastParticles.push({ + x: dummyX, + y: dummyY, + vx: Math.cos(angle) * spd, + vy: Math.sin(angle) * spd, + color: spell.color, + size: Math.random() * 5 + 3, + alpha: 1.0 + }); + } + + setTimeout(() => { + currentSpellIdx++; + nextNodeIdx = 0; + userPath = []; + + if (currentSpellIdx >= SPELLS.length) { + gameWon = true; + onDuelWon(); + } else { + updateSpellUI(); + } + }, 800); + } + + async function onDuelWon() { + if (window.soundFX) window.soundFX.playMagicSuccess(); + try { + const res = await fetch('/api/duel/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 = "⚡ Duell Gemeistert!"; + modalText.innerHTML = ` + Mit meisterhafter Zauberstabführung hast du alle 3 Zaubersprüche entfesselt und die verzauberte Rüstung bezwungen!

+ Gelehrte Sprüche: Flipendo ⚡, Incendio 🔥 & Alohomora 🗝️

+ Professor Flitwick applaudiert begeistert. Das uralte Schlosstor schwingt auf... + Dahinter flackern die unheimlichen Flammen von Snapes Flammenkammer!

+ 🎒 Das goldene Duellanten-Abzeichen wurde deinem Zauberbeutel hinzugefügt! + `; + modalBtn.innerText = "Weiter zu Snapes Flaschenrätsel 🧪"; + modalBtn.onclick = () => { window.location.href = '/bottles'; }; + modalBackdrop.classList.add('show'); + } + + function render() { + ctx.clearRect(0, 0, width, height); + + // Dungeon stone chamber background + const bgGrad = ctx.createRadialGradient(width * 0.5, height * 0.5, 30, width * 0.5, height * 0.5, width * 0.6); + bgGrad.addColorStop(0, '#1c172e'); + bgGrad.addColorStop(1, '#0c0a14'); + ctx.fillStyle = bgGrad; + ctx.fillRect(0, 0, width, height); + + // Stone floor tiles + ctx.strokeStyle = '#2d253d'; + ctx.lineWidth = 1; + for (let y = height * 0.75; y < height; y += 25) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(width, y); + ctx.stroke(); + } + + const spell = SPELLS[currentSpellIdx] || SPELLS[SPELLS.length - 1]; + + // Draw Spell Beam if casting + if (spellCastAnim > 0) { + ctx.save(); + ctx.strokeStyle = spell.beamColor; + ctx.lineWidth = 14 * spellCastAnim; + ctx.shadowColor = spell.color; + ctx.shadowBlur = 30; + ctx.beginPath(); + ctx.moveTo(width * 0.45, height * 0.5); + ctx.lineTo(width * 0.78, height * 0.5); + ctx.stroke(); + ctx.restore(); + spellCastAnim -= 0.05; + } + + // Draw Enchanted Target Dummy (Right side) + ctx.save(); + const dummyBaseX = width * 0.78 + (dummyShake > 0 ? (Math.random() - 0.5) * dummyShake : 0); + const dummyBaseY = height * 0.5; + if (dummyShake > 0) dummyShake *= 0.85; + + // Dummy wooden post & shield + ctx.fillStyle = '#4a3319'; + ctx.fillRect(dummyBaseX - 8, dummyBaseY - 60, 16, 120); + + // Armor Breastplate + ctx.fillStyle = '#6c757d'; + ctx.strokeStyle = '#e5b948'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(dummyBaseX, dummyBaseY - 10, 32, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + + // Armor Helmet + ctx.fillStyle = '#495057'; + ctx.beginPath(); + ctx.arc(dummyBaseX, dummyBaseY - 50, 20, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + + // Glowing Rune Shield Aura on Dummy + ctx.strokeStyle = spell.color; + ctx.shadowColor = spell.color; + ctx.shadowBlur = 15; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(dummyBaseX, dummyBaseY - 10, 42 + Math.sin(Date.now() * 0.008) * 4, 0, Math.PI * 2); + ctx.stroke(); + ctx.restore(); + + // Draw Spell Tracing Guide Lines (Left / Center side) + if (spellNodes.length > 1 && !gameWon) { + ctx.save(); + ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; + ctx.setLineDash([8, 8]); + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(spellNodes[0].x, spellNodes[0].y); + for (let i = 1; i < spellNodes.length; i++) { + ctx.lineTo(spellNodes[i].x, spellNodes[i].y); + } + ctx.stroke(); + ctx.restore(); + } + + // Draw Player's Drawn Line + if (userPath.length > 1) { + ctx.save(); + ctx.strokeStyle = spell.color; + ctx.lineWidth = 6; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.shadowColor = spell.color; + ctx.shadowBlur = 18; + ctx.beginPath(); + ctx.moveTo(userPath[0].x, userPath[0].y); + for (let i = 1; i < userPath.length; i++) { + ctx.lineTo(userPath[i].x, userPath[i].y); + } + ctx.stroke(); + ctx.restore(); + } + + // Draw Spell Nodes (Rune Stars to connect) + if (!gameWon) { + spellNodes.forEach((node, idx) => { + ctx.save(); + const isNext = (idx === nextNodeIdx); + const isHit = node.hit; + + ctx.shadowColor = isHit ? '#2ecc71' : (isNext ? spell.color : '#888'); + ctx.shadowBlur = isNext ? 20 : 8; + + ctx.fillStyle = isHit ? '#2ecc71' : (isNext ? '#ffffff' : '#332a42'); + ctx.strokeStyle = isNext ? spell.color : '#e5b948'; + ctx.lineWidth = isNext ? 4 : 2; + + const r = node.radius + (isNext ? Math.sin(Date.now() * 0.01) * 3 : 0); + ctx.beginPath(); + ctx.arc(node.x, node.y, r, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + + // Number label + ctx.fillStyle = isHit ? '#000' : (isNext ? '#000' : '#e5b948'); + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(idx + 1, node.x, node.y); + + ctx.restore(); + }); + } + + // Update & Render Wand Sparks + for (let i = wandParticles.length - 1; i >= 0; i--) { + const p = wandParticles[i]; + p.x += p.vx; + p.y += p.vy; + p.alpha *= 0.90; + if (p.alpha <= 0.02) { + wandParticles.splice(i, 1); + continue; + } + ctx.fillStyle = p.color; + ctx.globalAlpha = p.alpha; + ctx.beginPath(); + ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); + ctx.fill(); + } + + // Update & Render Blast Particles + for (let i = blastParticles.length - 1; i >= 0; i--) { + const p = blastParticles[i]; + p.x += p.vx; + p.y += p.vy; + p.alpha *= 0.92; + if (p.alpha <= 0.02) { + blastParticles.splice(i, 1); + continue; + } + ctx.fillStyle = p.color; + ctx.globalAlpha = p.alpha; + ctx.beginPath(); + ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); + ctx.fill(); + } + ctx.globalAlpha = 1.0; + + requestAnimationFrame(render); + } + requestAnimationFrame(render); +})(); diff --git a/app/templates/base.html b/app/templates/base.html index a298c57..b9de723 100755 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -119,12 +119,12 @@ {{ state.broom_score }} Pkt
- 🧹 Besenflug: + 🧹 Ring-Flug: {% if state.broom_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %}
- ♟️ Zauberschach: - {% if state.chess_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %} + 🪄 Spruch-Duell: + {% if state.duel_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %}
🧪 Snapes Flammen: @@ -150,15 +150,15 @@ 🧹
- Kammer I: Der Besenflug - {% if state.broom_solved %}✅ Gemeistert{% else %}🗝️ Geflügelte Schlüssel jagen{% endif %} + Kammer I: Der Ring-Flug + {% if state.broom_solved %}✅ Gemeistert{% else %}⭕ Goldene Ringe durchfliegen{% endif %}
- - ♟️ + + 🪄
- Kammer II: McGonagalls Schach - {% if state.chess_solved %}✅ Gemeistert{% else %}⚔️ Riesen-Zauberschach{% endif %} + Kammer II: Das Spruch-Duell + {% if state.duel_solved %}✅ Gemeistert{% else %}⚡ Zaubergesten zeichnen{% endif %}
@@ -169,6 +169,7 @@
+ 🧩
diff --git a/app/templates/broom.html b/app/templates/broom.html index 8c90b83..3139222 100755 --- a/app/templates/broom.html +++ b/app/templates/broom.html @@ -1,22 +1,23 @@ {% extends "base.html" %} -{% block title %}Kammer I: Der Flug der geflügelten Schlüssel · Hogwarts Escape{% endblock %} +{% block title %}Kammer I: Der Goldene Ring-Flug · Hogwarts Escape{% endblock %} {% block content %}
-

🧹 Kammer I: Der Besenflug 🗝️

+

🧹 Kammer I: Der Goldene Ring-Flug ⭕

- "Hunderte metallischer Flügel schwirren wie Vögel unter der Decke des Gewölbes.
- Die schwere Kerkertür am anderen Ende ist fest verschlossen.
- Nur ein einziger Schlüssel passt: Ein alter, großer Silberschlüssel mit geknicktem blauem Flügel!" + "Schwinge dich auf deinen Rennbesen über den Dächern von Hogwarts!
+ Durchfliege die magisch schwebenden goldenen Ringe, halte die Combo aufrecht
+ und meistere den Parcours von Madam Hooch, um die nächste Pforte zu öffnen!"

⏱️ Zeit: 0s
+
⭕ Ringe: 0/12
✨ Punkte: 0
-
🎯 Ziel: Schlüssel fangen!
+
@@ -39,25 +40,25 @@ ⚡ TURBO
- 💡 Tipp: Du kannst auch direkt mit dem Finger über das Spielfeld wischen! + 💡 Ziehe den Besen einfach mit dem Finger direkt von Ring zu Ring!

- 🎮 Steuerung: Pfeiltasten / WASD / Touch-Wischen zum Fliegen · Leertaste / Turbo für Besen-Beschleunigung! + 🎮 Steuerung: Wische mit dem Finger über das Spielfeld oder nutze Pfeiltasten / D-Pad zum Fliegen · Turbo für rasanten Schub!

- Fange den großen Silberschlüssel mit dem geknickten Flügel und fliege zur schweren Holztür ganz rechts! + Sammle 12 Ringe, um den Parcours abzuschließen. Schnelligkeit und Combos bringen Zusatzpunkte!

- Direkt weiter zu McGonagalls Zauberschach ♟️ ➔ + Direkt weiter zum Zauberspruch-Duell 🪄 ➔
{% endblock %} {% block scripts %} - + {% endblock %} diff --git a/app/templates/duel.html b/app/templates/duel.html new file mode 100755 index 0000000..714b019 --- /dev/null +++ b/app/templates/duel.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block title %}Kammer II: Das Zauberspruch-Duell · Hogwarts Escape{% endblock %} + +{% block content %} +
+

🪄 Kammer II: Das Zauberspruch-Duell ⚡

+

+ "Eine uralte verzauberte Rüstung bewacht die Pforte zur Flammenkammer.
+ Zücke deinen Zauberstab! Verbinde die leuchtenden Runensterne mit dem Finger oder der Maus,
+ um mächtige Hogwarts-Zaubersprüche zu entfesseln und die Rüstung zu bezwingen!" +

+
+ +
+
+
🪄 Spruch: Flipendo ⚡
+
Zauberspruch 1 von 3
+
🛡️ Ziel: 3 Zauber übrig
+
+ + +
+ +
+

+ Verbinde die leuchtenden Runensterne von 1 bis 4, um den Zauber zu wirken! +

+

+ 💡 Tipp: Ziehe deinen Finger oder die Maus flüssig durch die nummerierten Sterne auf dem Bildschirm. +

+
+ +
+ + Direkt weiter zu Snapes Flammenkammer 🧪 ➔ + +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 23bc6ec..828405a 100755 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -21,34 +21,35 @@

- +
{% if state.broom_solved %}✅ Gemeistert{% else %}🔓 Bereit{% endif %} -
🧹🗝️
-

Kammer I: Der Besenflug

+
🧹⭕
+

Kammer I: Der Ring-Flug

- Professor Flitwicks verzauberte Kammer. Schwärme geflügelter Schlüssel surren unter der hohen Gewölbedecke. - Finde den großen, alten Silberschlüssel mit dem geknickten Flügel und schließe die Tür auf! + Madam Hoochs Quidditch-Parcours! Schwinge dich auf deinen Rennbesen, steuere geschmeidig durch + die schwebenden goldenen Ringe und sammle Punkte und Speed-Combos!

- Auf den Besen schwingen 🧹 + Parcours fliegen 🧹
- +
- - {% if state.chess_solved %}✅ Gemeistert{% else %}⚔️ Bereit{% endif %} + + {% if state.duel_solved %}✅ Gemeistert{% else %}⚡ Bereit{% endif %} -
♟️⚔️
-

Kammer II: McGonagalls Schach

+
🪄⚡
+

Kammer II: Das Spruch-Duell

- Das gigantische, lebendige Zauberschach. Übernimm Rons Rolle als schwarzer Springer, - überliste die weiße Königin und setze den feindlichen König schachmatt! + Zücke deinen Zauberstab! Zeichne die magischen Runen-Gesten (Flipendo, Incendio & Alohomora) + flüssig mit dem Finger nach und zerschmettere die verzauberte Trainingsrüstung!

- Schachbrett betreten ♟️ + Duell beginnen 🪄
+
diff --git a/test_app.py b/test_app.py index 86f92fe..f8199f8 100755 --- a/test_app.py +++ b/test_app.py @@ -13,11 +13,16 @@ def test_endpoints(): res = client.get("/broom") assert res.status_code == 200 - assert "Der Besenflug" in res.text + assert "Der Goldene Ring-Flug" in res.text - res = client.get("/chess") + res = client.get("/duel") assert res.status_code == 200 - assert "McGonagalls Zauberschach" in res.text + assert "Das Zauberspruch-Duell" in res.text + + # Test /chess redirects to /duel + res = client.get("/chess", follow_redirects=False) + assert res.status_code in [301, 302] + assert res.headers["location"] == "/duel" res = client.get("/bottles") assert res.status_code == 200 @@ -43,24 +48,23 @@ def test_endpoints(): assert "snape_scroll" in item_ids # Test House API - res = client.post("/api/player/house", json={"house": "slytherin"}) + res = client.post("/api/player/house", json={"house": "ravenclaw"}) assert res.status_code == 200 - assert res.json()["house"]["name"] == "Slytherin" + assert res.json()["house"]["name"] == "Ravenclaw" - # Test API Broom complete (adds silver_key) - res = client.post("/api/broom/complete", json={"score": 500, "time_taken": 25}) + # Test API Broom complete + res = client.post("/api/broom/complete", json={"score": 850, "time_taken": 22}) assert res.status_code == 200 data = res.json() 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") + # Test API Duel complete (adds duel_badge) + res = client.post("/api/duel/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"] + duel_data = res.json() + assert duel_data["success"] is True + assert duel_data["new_item"]["id"] == "duel_badge" + assert "duel_badge" in duel_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"}) @@ -75,7 +79,7 @@ def test_endpoints(): assert res.status_code == 200 assert res.json()["success"] is True - print("ALL TESTS (INCLUDING CHESS & MOBILE FLIGHT) PASSED! ♟️🧹✨") + print("ALL TESTS (RING FLIGHT & SPELL DUEL) PASSED! ⭕🪄✨") if __name__ == "__main__": test_endpoints()