From efb54befccc43f5fe76cc87fba27a74736160629 Mon Sep 17 00:00:00 2001 From: Maxi Date: Tue, 22 Sep 2026 19:24:26 +0200 Subject: [PATCH] Fix mobile broom flight with D-pad & touch-action, add Chamber II McGonagalls Wizard Chess --- app/__pycache__/main.cpython-313.pyc | Bin 15810 -> 17427 bytes app/main.py | 50 +++++- app/static/css/escape.css | 222 ++++++++++++++++++++++-- app/static/js/broom.js | 240 ++++++++++++++++---------- app/static/js/chess.js | 247 +++++++++++++++++++++++++++ app/templates/base.html | 14 +- app/templates/broom.html | 35 ++-- app/templates/chess.html | 68 ++++++++ app/templates/index.html | 19 ++- test_app.py | 14 +- 10 files changed, 793 insertions(+), 116 deletions(-) create mode 100755 app/static/js/chess.js create mode 100755 app/templates/chess.html diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc index 91b5932174d64f43854cc50c24c698390ecbddb5..715350d4c13b3c3595ef32605784f0d2e6c307a7 100644 GIT binary patch delta 4094 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 U%+WI=OVj;})021gNO-dU165w?NB{r; delta 3010 zcma)7U2q%K72aK~BwIhR6gy6A$FcmY5m3cAb{YpK{*CRJ*iO7w;v`OFSz6n2q?K}a zO%f=W1Q>pX>5$$T2z@99W`NLXI-m|PeWB&44+N6w@S;f{n9g*{Pe83hhjjYTbI!_f zq%uQyX1~4nobR4{&b{ZH<p+=yBz{N?04@jhB^e{uQ*wIocY1(&UgF|N((_N zt5 zGCtkdE-MGCg+-Y)k>t>HqcSnwOqvo!;a^I%Ws}pTN~lvpeY>O_=9QM_vLgkuDLw+S zqXn{K{4q;&?ePNHH0}hU(ZlZLIcam*i2}w+{x?grH{2nbocx|MyjpHeMq|9l(qaK`#~P3S`i`YU z0i%6h3?wr*tWz@o&hgG>D{Fx8**ysNAp{Xx`A5!gJZ(7KiLeV{H~)jH-r<6w1>V(a z{*|jijPVWbF@Dm0!s-kO69ODcg~Jc(@n_ej9xF^WQ7H+D55Q4Jj&o_l2i1x@KLxEK zg+#@TtEN1r2|iKQ=-M!rwW)eEl2%z8KVMcSI{B+*gZ#U)p6Y$TWrGOh?tUD#A`I}O zo>8%jzv(&dJO+YEc=dk?-0cnWyYUW3q~?=*}}-=CXi?C`K6cb41>wKPA{qK zTsR$Jk)+N-xPA%o=$1HN*k`(GhGCp4wqLW_FPaeU_zCNq(s2`-x(QNka~^Mf4znkjk4cwoYWW1ls42b{c_YepY1_ z4;0yT&87?^q3SFf-cd=WfYDi8q5LA_(X;EMBjKS(J7~5n~7{fNleTtb@l) zJnvdmaqS^hGg9mvy8x6Ak^3IMTK^QEY1r24wpZC!1-r|h6YN&|?SEPYZ=ZNu$ArJo z)b9}AaQ9o~_YwXm_u2X#nJW#mqRlK|UfI;WjP*4ckg|4tDH>IEor*h9nw1yzl$I^s zcQ&e~jd)6Ws?Fu&l7sjF)PRqLeAhtQ?CNk!%UIf^a-W^{>moUk{HiO;_DU zm{{!Rp#6k@-t-S@pmb|r^Bc_}zN6&?G`5Q^JvG#Bdw@4+-VgPMKA5v^{&h>;;4?7H z+LI6jlx(1cjic%y!XbnS1O)+`p4k}4X_1`Pq4x@K_WY24lj9dKz5%bU@wDI1f9d!C zWV5T@_W01#6RqYfa^XZ1V zVE9sVbE;M!kQv6zE&^mrH1$k44o#I#J5b7=MHMv^>W!ATW|*0I7!`N!jLpCl+BK!; z^QgjBh+Xim+uDf9NNozbHMDIN>g;7;+!Xj{_goQA@+a>-o#_f5m7VLw|0G{&YixcB zSd(TE(QzDr`CZ}CPpLce_cp)S_z0+=PUsqKOx2eXM!v&?=NKb!Fx`h4WVs zF7jLLf7z)d`oAY86)jI z#rrxNYUxKo^_uUu;TVf+r`TJtkDNK*`I*Ir4FuA3de7m$7hnkIO*^mO!vA!&3Ow?B zpItxnw_))RzqRL}w<9a1bThpvvWZM+uOu$NjvLJ&VDEuSj7JS~FDeK8$C_DyKQsF0 z(9eN7IW=ACpVVryayq3ORGAD5f^M9GV(7+YfgMLFMPJgDOvRQGs(UZ{5QJDu`UjTG z&F=joAMdR!?H2=X>G9Is6xVu3%4@{tOJ{NdeBNrmg`eDR{&DX)p6q+Q(k0qgj;sps uS+xjKO-_PQZYyu;-&wUmJSeXGbXCAFD!1e$9Ois{sXxg7)ITJnL;nSgOvhUQ 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 = "
  • Partie eröffnet: Schwarz am Zug!
  • "; + 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 = `1... ♞f2+ (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 = `1... ♛xh2# (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 = "⚠️ Kein Schachmatt! Die weißen Figuren schlagen zurück. Drücke auf 'Zurücksetzen' und versuche es erneut!"; + statusMsg.style.color = "#e67e22"; + } + } + } + + async function triggerWin(moveNotation, explanation) { + isSolved = true; + if (window.soundFX) window.soundFX.playMagicSuccess(); + + if (statusMsg) { + statusMsg.innerHTML = `🏆 ${moveNotation} — SCHACHMATT! 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 = ` + ${moveNotation} — Grandios kombiniert!

    + ${explanation}

    + Die weiße Königin wirft ihr Schwert zu Boden. Die Riesen-Schachfiguren weichen zur Seite und die Tür zur Flammenkammer (Snapes Flaschenrätsel) steht weit offen!

    + 🎒 Ein massiver schwarzer Springer wurde deinem Zauberbeutel hinzugefügt! + `; + 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(); +})(); diff --git a/app/templates/base.html b/app/templates/base.html index e036f43..a298c57 100755 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -122,6 +122,10 @@ 🧹 Besenflug: {% if state.broom_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %} +
    + ♟️ Zauberschach: + {% if state.chess_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %} +
    🧪 Snapes Flammen: {% if state.bottles_solved %}✅ Gelöst{% else %}⏳ Offen{% endif %} @@ -150,13 +154,21 @@ {% if state.broom_solved %}✅ Gemeistert{% else %}🗝️ Geflügelte Schlüssel jagen{% endif %}
    + + ♟️ +
    + Kammer II: McGonagalls Schach + {% if state.chess_solved %}✅ Gemeistert{% else %}⚔️ Riesen-Zauberschach{% endif %} +
    +
    🧪
    - Kammer II: Snapes Flaschen + Kammer III: Snapes Flaschen {% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Die Flammenkammer{% endif %}
    + 🧩
    diff --git a/app/templates/broom.html b/app/templates/broom.html index 33776af..8c90b83 100755 --- a/app/templates/broom.html +++ b/app/templates/broom.html @@ -16,33 +16,48 @@
    ⏱️ Zeit: 0s
    ✨ Punkte: 0
    -
    🎯 Mission: Fange den Schlüssel mit geknicktem Flügel!
    +
    🎯 Ziel: Schlüssel fangen!
    - -
    - - - + +
    +
    + +
    + +
    ✨
    + +
    + +
    + +
    + +
    + 💡 Tipp: Du kannst auch direkt mit dem Finger über das Spielfeld wischen! +
    +

    - 🎮 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!

    {% endblock %} {% block scripts %} - + {% endblock %} diff --git a/app/templates/chess.html b/app/templates/chess.html new file mode 100755 index 0000000..afcbdd5 --- /dev/null +++ b/app/templates/chess.html @@ -0,0 +1,68 @@ +{% extends "base.html" %} + +{% block title %}Kammer II: McGonagalls Zauberschach · Hogwarts Escape{% endblock %} + +{% block content %} +
    +

    ♟️ Kammer II: McGonagalls Zauberschach ⚔️

    +

    + "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! +

    +
    + +
    +
    + 🧙‍♂️ Wähle deinen schwarzen Springer oder deine Dame und führe den Siegeszug aus! +
    + + +
    +
    + +
    +
    + +
    + + + 🧪 Zu Snapes Flaschenkammer ➔ + +
    + + +
    + 📜 Zug-Protokoll: +
      +
    • Partie eröffnet: Schwarz am Zug!
    • +
    +
    +
    + + +
    +
    + + 💡 Rons taktischer Schach-Tipp (Hier klicken) + +
    +

    + 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! +

    +
    +
    +
    +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 8575ab5..23bc6ec 100755 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -35,13 +35,27 @@ Auf den Besen schwingen 🧹
    - + +
    + + {% if state.chess_solved %}✅ Gemeistert{% else %}⚔️ Bereit{% endif %} + +
    ♟️⚔️
    +

    Kammer II: McGonagalls Schach

    +

    + 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 ♟️ +
    + +
    {% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Bereit{% endif %}
    🧪🔥
    -

    Kammer II: Snapes Flaschenrätsel

    +

    Kammer III: Snapes Flaschen

    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 🧪

    +
    diff --git a/test_app.py b/test_app.py index 40030f8..86f92fe 100755 --- a/test_app.py +++ b/test_app.py @@ -15,6 +15,10 @@ def test_endpoints(): assert res.status_code == 200 assert "Der Besenflug" in res.text + res = client.get("/chess") + assert res.status_code == 200 + assert "McGonagalls Zauberschach" in res.text + res = client.get("/bottles") assert res.status_code == 200 assert "Snapes Flaschenrätsel" in res.text @@ -50,6 +54,14 @@ def test_endpoints(): assert data["success"] is True assert "silver_key" in data["state"]["inventory"] + # Test API Chess complete (adds chess_knight) + res = client.post("/api/chess/complete") + assert res.status_code == 200 + chess_data = res.json() + assert chess_data["success"] is True + assert chess_data["new_item"]["id"] == "chess_knight" + assert "chess_knight" in chess_data["state"]["inventory"] + # Test API Bottles drink: Ahead / Black fire (Bottle 3 - Dwarf, adds philosophers_stone) res = client.post("/api/bottles/drink", json={"bottle": 3, "gate": "ahead"}) assert res.status_code == 200 @@ -63,7 +75,7 @@ def test_endpoints(): assert res.status_code == 200 assert res.json()["success"] is True - print("ALL INVENTORY & GAMEPLAY TESTS PASSED! 🎒✨") + print("ALL TESTS (INCLUDING CHESS & MOBILE FLIGHT) PASSED! ♟️🧹✨") if __name__ == "__main__": test_endpoints()