diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc index 7ef74bf..38fc24a 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 8f7e5f2..2e7bb72 100755 --- a/app/main.py +++ b/app/main.py @@ -48,6 +48,14 @@ ALL_ITEMS = { "action": "broom_polish", "action_label": "Besen polieren ✨" }, + "winged_key": { + "id": "winged_key", + "name": "Geflügelter Schlüssel", + "icon": "🗝️", + "desc": "Ein alter Silberschlüssel mit zarten Libellenflügeln. Er sperrt die Eichentür zur Duellier-Kammer auf!", + "action": "flutter_key", + "action_label": "Flügel surren lassen 🪽" + }, "duel_badge": { "id": "duel_badge", "name": "Duellanten-Abzeichen", @@ -130,6 +138,8 @@ 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") and "winged_key" not in state["inventory"]: + state["inventory"].append("winged_key") if state.get("duel_solved") and "duel_badge" not in state["inventory"]: state["inventory"].append("duel_badge") if state.get("brew_solved") and "felix_felicis" not in state["inventory"]: @@ -153,17 +163,94 @@ def get_session_state(request: Request) -> dict: request.session["escape"] = state return request.session["escape"] -def get_template_context(request: Request, state: dict, lang: str) -> dict: +CHAMBER_REQUIREMENTS = { + "duel": { + "item_id": "winged_key", + "item_name": "Geflügelter Schlüssel", + "item_icon": "🗝️", + "from_chamber": "Kammer I: Der Ring-Flug", + "from_url": "/broom", + "reason": "Die schwere Eichentür zur Duellier-Kammer ist mit einem verzauberten Flügelschloss versiegelt. Fange zuerst den geflügelten Schlüssel im Besenflug!" + }, + "brew": { + "item_id": "duel_badge", + "item_name": "Duellanten-Abzeichen & Kessel-Rezept", + "item_icon": "⚡", + "from_chamber": "Kammer II: Das Spruch-Duell", + "from_url": "/duel", + "reason": "Snapes Kupferkessel bleibt kalt. Du benötigst die geheime Rezeptur aus der bezwungenen Trainingsrüstung des Duellier-Clubs!" + }, + "cryptex": { + "item_id": "felix_felicis", + "item_name": "Felix Felicis", + "item_icon": "🧪", + "from_chamber": "Kammer III: Die Brauküche", + "from_url": "/brew", + "reason": "Ein uralter Verwirrungsbann verschleiert die Runen des Gründerschlosses. Braue 'Felix Felicis', um die Symbole der vier Gründer zu durchschauen!" + }, + "pixies": { + "item_id": "cryptex_key", + "item_name": "Schlüssel der Gründer", + "item_icon": "🗝️", + "from_chamber": "Kammer IV: Das Runen-Drehschloss", + "from_url": "/cryptex", + "reason": "Das Verteidigungs-Klassenzimmer ist mit dem uralten Siegel der vier Gründer verriegelt. Knacke das Drehschloss in Kammer IV!" + }, + "bottles": { + "item_id": "cryptex_key", + "item_name": "Schlüssel der Gründer", + "item_icon": "🗝️", + "from_chamber": "Kammer V: Wichtel-Panik", + "from_url": "/pixies", + "reason": "Tödliche Flammen und schwere Ketten versperren den Eingang. Schließe zuerst die Wichtel-Prüfung ab und banne die Plagegeister!" + }, + "finale": { + "item_id": "philosophers_stone", + "item_name": "Stein der Weisen", + "item_icon": "💎", + "from_chamber": "Kammer VI: Snapes Flaschen", + "from_url": "/bottles", + "reason": "Der Spiegel Nerhegeb offenbart sich nur jenem Zauberer, der Snapes schwarzes Feuer erfolgreich durchquert hat!" + } +} + +def get_template_context(request: Request, state: dict, lang: str, chamber_name: str = None) -> dict: inv_ids = state.get("inventory", ["wand", "broom", "snape_scroll"]) inv_items = [ALL_ITEMS[i] for i in inv_ids if i in ALL_ITEMS] house_info = HOUSES.get(state.get("house", "gryffindor"), HOUSES["gryffindor"]) + + is_locked = False + lock_info = None + has_alohomora = request.query_params.get("alohomora") == "1" or state.get("alohomora_mode") + if request.query_params.get("alohomora") == "1": + state["alohomora_mode"] = True + request.session["escape"] = state + + req = CHAMBER_REQUIREMENTS.get(chamber_name) + if req and not has_alohomora: + has_item = req["item_id"] in inv_ids + solved_bypass = { + "duel": state.get("broom_solved"), + "brew": state.get("duel_solved"), + "cryptex": state.get("brew_solved"), + "pixies": state.get("cryptex_solved"), + "bottles": state.get("pixies_solved"), + "finale": state.get("bottles_solved") + } + if not has_item and not solved_bypass.get(chamber_name, False): + is_locked = True + lock_info = req + return { "state": state, "lang": lang, "inventory_items": inv_items, "all_items": ALL_ITEMS, "house": house_info, - "houses": HOUSES + "houses": HOUSES, + "is_locked": is_locked, + "lock_info": lock_info, + "chamber_requirements": CHAMBER_REQUIREMENTS } @app.get("/", response_class=HTMLResponse) @@ -187,7 +274,7 @@ async def page_broom(request: Request): return templates.TemplateResponse( request=request, name="broom.html", - context=get_template_context(request, state, lang) + context=get_template_context(request, state, lang, chamber_name="broom") ) @app.get("/duel", response_class=HTMLResponse) @@ -199,7 +286,7 @@ async def page_duel(request: Request): return templates.TemplateResponse( request=request, name="duel.html", - context=get_template_context(request, state, lang) + context=get_template_context(request, state, lang, chamber_name="duel") ) @app.get("/brew", response_class=HTMLResponse) @@ -211,7 +298,7 @@ async def page_brew(request: Request): return templates.TemplateResponse( request=request, name="brew.html", - context=get_template_context(request, state, lang) + context=get_template_context(request, state, lang, chamber_name="brew") ) @app.get("/cryptex", response_class=HTMLResponse) @@ -223,7 +310,7 @@ async def page_cryptex(request: Request): return templates.TemplateResponse( request=request, name="cryptex.html", - context=get_template_context(request, state, lang) + context=get_template_context(request, state, lang, chamber_name="cryptex") ) @app.get("/pixies", response_class=HTMLResponse) @@ -235,7 +322,7 @@ async def page_pixies(request: Request): return templates.TemplateResponse( request=request, name="pixies.html", - context=get_template_context(request, state, lang) + context=get_template_context(request, state, lang, chamber_name="pixies") ) @app.get("/chess") @@ -252,7 +339,7 @@ async def page_bottles(request: Request): return templates.TemplateResponse( request=request, name="bottles.html", - context=get_template_context(request, state, lang) + context=get_template_context(request, state, lang, chamber_name="bottles") ) @app.get("/sliding", response_class=HTMLResponse) @@ -264,7 +351,7 @@ async def page_sliding(request: Request): return templates.TemplateResponse( request=request, name="sliding.html", - context=get_template_context(request, state, lang) + context=get_template_context(request, state, lang, chamber_name="sliding") ) @app.get("/finale", response_class=HTMLResponse) @@ -276,7 +363,7 @@ async def page_finale(request: Request): return templates.TemplateResponse( request=request, name="finale.html", - context=get_template_context(request, state, lang) + context=get_template_context(request, state, lang, chamber_name="finale") ) @app.get("/api/state") @@ -348,10 +435,15 @@ 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)) + if "inventory" not in state: + state["inventory"] = ["wand", "broom", "snape_scroll"] + if "winged_key" not in state["inventory"]: + state["inventory"].append("winged_key") request.session["escape"] = state return JSONResponse({ "success": True, - "message": "Parcours gemeistert! Die Tür zum Zauberspruch-Duell steht offen!", + "message": "Parcours gemeistert! Der geflügelte Schlüssel öffnet die Tür zum Zauberspruch-Duell!", + "new_item": ALL_ITEMS["winged_key"], "state": state }) diff --git a/app/static/css/escape.css b/app/static/css/escape.css index e060f8a..6ce0f66 100755 --- a/app/static/css/escape.css +++ b/app/static/css/escape.css @@ -1517,6 +1517,215 @@ body { } +/* ======================================================== + MINI-GAME: DIE BRAUKÜCHE / CAULDRON BREW (/brew) + ======================================================== */ +.brew-container { + max-width: 520px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + padding: 0.5rem; +} + +.recipe-book { + width: 100%; + background: rgba(22, 17, 30, 0.9); + border: 2px solid var(--gold); + border-radius: 14px; + padding: 1rem 1.2rem; + text-align: center; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6); +} + +.recipe-step-badge { + display: inline-block; + background: rgba(229, 185, 72, 0.2); + border: 1px solid var(--gold); + color: var(--gold); + font-size: 0.8rem; + font-weight: bold; + padding: 0.2rem 0.75rem; + border-radius: 20px; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 0.3rem; +} + +.cauldron-visual-container { + width: 100%; + max-width: 320px; + aspect-ratio: 320 / 280; + margin: 0 auto; + position: relative; + display: flex; + justify-content: center; + align-items: center; + filter: drop-shadow(0 10px 25px rgba(0, 0, 0, 0.8)); +} + +.cauldron-svg { + width: 100%; + height: 100%; + overflow: visible; +} + +.animated-flames { + animation: flameFlicker 0.6s infinite alternate ease-in-out; + transform-origin: 160px 260px; +} + +@keyframes flameFlicker { + 0% { transform: scaleY(0.92) scaleX(0.98); opacity: 0.85; } + 100% { transform: scaleY(1.08) scaleX(1.02); opacity: 1; } +} + +.bubble-anim { + animation: bubbleRise 1.6s infinite ease-in-out alternate; +} +.bubble-anim.b1 { animation-delay: 0.1s; } +.bubble-anim.b2 { animation-delay: 0.4s; } +.bubble-anim.b3 { animation-delay: 0.7s; } +.bubble-anim.b4 { animation-delay: 1.0s; } +.bubble-anim.b5 { animation-delay: 1.3s; } + +@keyframes bubbleRise { + 0% { transform: translateY(0); opacity: 0.4; } + 50% { transform: translateY(-4px) scale(1.2); opacity: 0.8; } + 100% { transform: translateY(2px) scale(0.9); opacity: 0.5; } +} + +/* Ingredients Shelf */ +.ingredients-shelf { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.65rem; + width: 100%; +} + +.ingredient-btn { + background: linear-gradient(135deg, rgba(30, 24, 42, 0.95), rgba(18, 14, 26, 0.95)); + border: 2px solid rgba(229, 185, 72, 0.35); + border-radius: 12px; + padding: 0.85rem 0.5rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.3rem; + cursor: pointer; + font-family: inherit; + transition: all 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28); + touch-action: manipulation; + -webkit-user-select: none; + user-select: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); +} + +.ingredient-btn:active { + transform: scale(0.94); + border-color: var(--gold); +} + +.ingredient-btn:disabled { + opacity: 0.45; + cursor: not-allowed; + border-color: rgba(255, 255, 255, 0.1); + transform: none !important; +} + +.ing-icon { + font-size: 2.2rem; + filter: drop-shadow(0 2px 5px rgba(0,0,0,0.6)); +} + +.ing-name { + color: var(--text-light); + font-size: 0.88rem; + font-weight: bold; +} + +.ing-count { + color: var(--gold); + font-size: 0.78rem; + background: rgba(0, 0, 0, 0.4); + padding: 0.1rem 0.45rem; + border-radius: 8px; +} + +/* Heat Control Panel */ +.heat-control-panel { + display: none; + flex-direction: column; + align-items: center; + gap: 0.6rem; + width: 100%; + background: rgba(20, 16, 28, 0.9); + border: 2px solid var(--gold); + border-radius: 14px; + padding: 1rem 1.2rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6); +} + +.heat-control-panel.active { + display: flex; +} + +.temp-bar-container { + width: 100%; + max-width: 320px; + height: 24px; + background: rgba(10, 8, 16, 0.9); + border: 2px solid #555; + border-radius: 12px; + position: relative; + overflow: hidden; +} + +.temp-ideal-zone { + position: absolute; + top: 0; + bottom: 0; + background: rgba(46, 204, 113, 0.35); + border-left: 2px solid #2ecc71; + border-right: 2px solid #2ecc71; + z-index: 1; +} + +.temp-indicator { + height: 100%; + width: 20%; + background: linear-gradient(90deg, #3498db, #f39c12, #e74c3c); + border-radius: 10px; + transition: width 0.15s ease-out, background 0.2s ease; + z-index: 2; + position: relative; +} + +.bellows-btn { + background: linear-gradient(135deg, #d35400, #c0392b); + border: 2px solid #f39c12; + border-radius: 12px; + color: #ffffff; + font-family: inherit; + font-size: 1.15rem; + font-weight: bold; + padding: 0.85rem 2rem; + cursor: pointer; + box-shadow: 0 4px 15px rgba(231, 76, 60, 0.4); + touch-action: manipulation; + -webkit-user-select: none; + user-select: none; + transition: transform 0.1s ease, box-shadow 0.1s ease; + margin-top: 0.3rem; +} + +.bellows-btn:active { + transform: scale(0.94); + box-shadow: 0 2px 8px rgba(231, 76, 60, 0.6); +} + /* ======================================================== MINI-GAME: WICHTEL-PANIK / CORNISH PIXIES (/pixies) ======================================================== */ @@ -1640,22 +1849,349 @@ body { 100% { transform: translateY(0) rotate(6deg); } } -/* Flight Start Overlay */ +/* Flight Canvas Wrapper & Start Overlay */ +.canvas-wrapper { + position: relative; + width: 100%; + max-width: 800px; + margin: 0 auto; + overflow: hidden; + border-radius: 14px; + box-shadow: 0 8px 30px rgba(0,0,0,0.6); +} + .flight-start-overlay { position: absolute; inset: 0; - background: rgba(12, 10, 20, 0.9); + background: rgba(12, 10, 20, 0.93); border: 2px solid var(--gold); border-radius: 14px; - z-index: 25; + z-index: 50; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 1.5rem; text-align: center; - backdrop-filter: blur(4px); - box-shadow: 0 0 30px rgba(0, 0, 0, 0.9); + backdrop-filter: blur(6px); + box-shadow: 0 0 35px rgba(0, 0, 0, 0.95); + pointer-events: auto; +} + +.flight-start-overlay.hidden { + display: none !important; + opacity: 0 !important; + pointer-events: none !important; + visibility: hidden !important; +} + +/* Chamber Locked Screen */ +.chamber-lock-banner { + background: linear-gradient(135deg, rgba(30, 20, 25, 0.95), rgba(18, 12, 18, 0.98)); + border: 2px dashed #e74c3c; + border-radius: 16px; + padding: 2.2rem 1.5rem; + text-align: center; + margin: 1.5rem auto; + max-width: 600px; + box-shadow: 0 0 35px rgba(231, 76, 60, 0.25); + position: relative; +} + +.chamber-lock-icon { + font-size: 3.6rem; + margin-bottom: 0.5rem; + animation: lockJiggle 2.5s infinite ease-in-out; +} + +@keyframes lockJiggle { + 0%, 100% { transform: rotate(0deg); } + 15% { transform: rotate(-9deg); } + 30% { transform: rotate(9deg); } + 45% { transform: rotate(-5deg); } + 60% { transform: rotate(0deg); } +} + +.chamber-lock-title { + color: #e74c3c; + font-size: 1.7rem; + margin-bottom: 0.6rem; +} + +.chamber-lock-desc { + color: var(--text-light); + font-size: 1.05rem; + line-height: 1.55; + margin-bottom: 1.3rem; +} + +.chamber-req-box { + display: inline-flex; + align-items: center; + gap: 0.9rem; + background: rgba(0, 0, 0, 0.55); + border: 2px solid var(--gold); + border-radius: 12px; + padding: 0.85rem 1.4rem; + margin-bottom: 1.5rem; + text-align: left; + box-shadow: 0 0 20px rgba(229, 185, 72, 0.2); +} + +.chamber-req-icon { + font-size: 2.4rem; +} + +.chamber-req-info strong { + display: block; + color: var(--gold); + font-size: 1.15rem; +} + +.chamber-req-info span { + color: var(--text-dim); + font-size: 0.88rem; +} + +.chamber-lock-actions { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.85rem; +} + +.alohomora-link { + color: var(--text-dim); + font-size: 0.85rem; + text-decoration: underline; + cursor: pointer; + transition: color 0.2s; + background: none; + border: none; + padding: 0.3rem 0.6rem; +} + +.alohomora-link:hover { + color: var(--gold); +} + +/* ======================================================== + VICTORY CELEBRATION MODAL & PARTICLES + ======================================================== */ +.victory-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(6, 4, 12, 0.94); + backdrop-filter: blur(10px); + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + opacity: 0; + pointer-events: none; + visibility: hidden; + transition: opacity 0.35s ease, visibility 0.35s ease; +} + +.victory-modal-backdrop.show { + opacity: 1; + pointer-events: auto; + visibility: visible; +} + +.victory-confetti-canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1; +} + +.victory-card { + position: relative; + z-index: 2; + background: linear-gradient(135deg, rgba(26, 18, 40, 0.98), rgba(14, 10, 22, 0.98)); + border: 3px solid var(--gold); + border-radius: 20px; + max-width: 520px; + width: 100%; + padding: 2rem 1.5rem; + text-align: center; + box-shadow: 0 0 50px rgba(229, 185, 72, 0.45), inset 0 0 30px rgba(0, 0, 0, 0.8); + transform: scale(0.85); + transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + max-height: 90vh; + overflow-y: auto; +} + +.victory-modal-backdrop.show .victory-card { + transform: scale(1); +} + +.victory-crest-wrap { + position: relative; + width: 85px; + height: 85px; + margin: 0 auto 0.75rem auto; + display: flex; + align-items: center; + justify-content: center; +} + +.victory-crest-icon { + font-size: 3.8rem; + filter: drop-shadow(0 0 20px #ffd700); + animation: victoryCrestBounce 1s infinite alternate ease-in-out; + position: relative; + z-index: 2; +} + +@keyframes victoryCrestBounce { + 0% { transform: translateY(0) scale(1); } + 100% { transform: translateY(-8px) scale(1.08); } +} + +.victory-chamber-badge { + display: inline-block; + background: rgba(229, 185, 72, 0.2); + border: 1px solid var(--gold); + color: var(--gold); + font-weight: bold; + padding: 0.25rem 0.85rem; + border-radius: 20px; + font-size: 0.85rem; + letter-spacing: 1px; + text-transform: uppercase; + margin-bottom: 0.4rem; +} + +.victory-title { + font-size: clamp(1.5rem, 5vw, 2.1rem); + color: #ffd700; + margin-bottom: 0.6rem; + text-shadow: 0 2px 10px rgba(255, 215, 0, 0.4); +} + +.victory-stats-box { + display: flex; + justify-content: center; + gap: 1.2rem; + margin-bottom: 1.2rem; + font-size: 0.95rem; + color: var(--text-light); + background: rgba(0, 0, 0, 0.35); + padding: 0.5rem 1rem; + border-radius: 10px; +} + +.victory-reward-box { + display: flex; + align-items: center; + gap: 1rem; + background: linear-gradient(135deg, rgba(229, 185, 72, 0.15), rgba(46, 204, 113, 0.1)); + border: 2px solid var(--gold); + border-radius: 14px; + padding: 0.85rem 1rem; + margin-bottom: 1.25rem; + text-align: left; + box-shadow: 0 0 20px rgba(229, 185, 72, 0.25); + animation: rewardGlow 2s infinite alternate; +} + +@keyframes rewardGlow { + 0% { box-shadow: 0 0 10px rgba(229, 185, 72, 0.2); } + 100% { box-shadow: 0 0 25px rgba(229, 185, 72, 0.5); } +} + +.reward-icon { + font-size: 2.8rem; + filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.8)); + animation: rewardPop 0.6s ease-out; +} + +@keyframes rewardPop { + 0% { transform: scale(0.4) rotate(-20deg); opacity: 0; } + 70% { transform: scale(1.25) rotate(10deg); } + 100% { transform: scale(1) rotate(0deg); opacity: 1; } +} + +.reward-label { + display: block; + font-size: 0.78rem; + color: var(--gold-light); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.reward-name { + display: block; + font-size: 1.15rem; + color: #ffffff; + margin: 0.1rem 0; +} + +.reward-desc { + font-size: 0.82rem; + color: var(--text-dim); + margin: 0; + line-height: 1.3; +} + +.victory-story-box { + font-family: 'Caveat', cursive; + font-size: 1.35rem; + color: #e0d0b8; + line-height: 1.5; + background: rgba(15, 10, 20, 0.6); + border-left: 3px solid var(--gold); + padding: 0.75rem 1rem; + margin-bottom: 1.5rem; + text-align: left; + border-radius: 0 8px 8px 0; +} + +.victory-actions { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.victory-next-btn { + font-size: 1.25rem !important; + padding: 0.95rem 1.5rem !important; + box-shadow: 0 0 25px rgba(229, 185, 72, 0.6) !important; + animation: nextBtnPulse 1.8s infinite ease-in-out; +} + +@keyframes nextBtnPulse { + 0%, 100% { transform: scale(1); box-shadow: 0 0 20px rgba(229, 185, 72, 0.5); } + 50% { transform: scale(1.03); box-shadow: 0 0 32px rgba(229, 185, 72, 0.85); } +} + +.victory-sub-actions { + display: flex; + justify-content: center; + gap: 1rem; +} + +.btn-subtle { + background: transparent; + border: 1px solid rgba(229, 185, 72, 0.4); + color: var(--text-light); + border-radius: 8px; + padding: 0.5rem 0.9rem; + font-size: 0.9rem; + cursor: pointer; + text-decoration: none; + transition: all 0.2s; +} + +.btn-subtle:hover { + background: rgba(229, 185, 72, 0.15); + border-color: var(--gold); + color: var(--gold); } /* Mobile responsive adjustments */ diff --git a/app/static/js/bottles.js b/app/static/js/bottles.js index ebb7a07..08022ce 100755 --- a/app/static/js/bottles.js +++ b/app/static/js/bottles.js @@ -123,19 +123,36 @@ const data = await res.json(); if (data.result === 'success_ahead') { - if (window.soundFX) window.soundFX.playMagicSuccess(); - if (window.notifyNewItem && data.new_item) { - window.notifyNewItem(data.new_item); + if (window.showChamberVictory) { + window.showChamberVictory({ + chamberBadge: "Kammer VI: Snapes Flaschenrätsel", + title: "🔥 Schwarzes Feuer Durchquert!", + statsHtml: `🧪 Flasche 3 (Zwerg): Schutztrank🔥 Schwarzes Feuer: Besiegt!`, + item: data.new_item || { + id: "philosophers_stone", + name: "Stein der Weisen", + icon: "💎", + desc: "Der legendäre rote Alchemistenstein!" + }, + storyText: "Ein eisiger Frosthauch hüllt deinen Körper ein! Du schreitest durch das tosend schwarze Feuer, ohne auch nur ein Haar zu versengen. Dahinter enthüllt sich das größte Geheimnis von Hogwarts: Der Spiegel Nerhegeb und der Stein der Weisen!", + nextUrl: "/finale", + nextBtnText: "➔ Zum großen Finale: Der Stein der Weisen 💎" + }); + } else { + if (window.soundFX) window.soundFX.playMagicSuccess(); + if (window.notifyNewItem && data.new_item) { + window.notifyNewItem(data.new_item); + } + modalIcon.innerText = "❄️⚡"; + modalTitle.innerText = "Hervorragend kombiniert!"; + modalTitle.style.color = "#ffd700"; + modalText.innerHTML = `Du hast die kleinste Flasche (Flasche 3, den 'Zwerg') getrunken!

+ ${data.message}

+ Das schwarze Feuer teilt sich mit einem frostigen Zischen. Der Weg zum Stein der Weisen liegt frei vor dir!`; + modalBtn.innerText = "Zum Finale & Escape-Triumph 🏆"; + modalBtn.onclick = () => { window.location.href = data.next_url; }; + modalBackdrop.classList.add('show'); } - modalIcon.innerText = "❄️⚡"; - modalTitle.innerText = "Hervorragend kombiniert!"; - modalTitle.style.color = "#ffd700"; - modalText.innerHTML = `Du hast die kleinste Flasche (Flasche 3, den 'Zwerg') getrunken!

- ${data.message}

- Das schwarze Feuer teilt sich mit einem frostigen Zischen. Der Weg zum Stein der Weisen liegt frei vor dir!`; - modalBtn.innerText = "Zum Finale & Escape-Triumph 🏆"; - modalBtn.onclick = () => { window.location.href = data.next_url; }; - modalBackdrop.classList.add('show'); } else if (data.result === 'success_back') { if (window.soundFX) window.soundFX.playMagicSuccess(); diff --git a/app/static/js/brew.js b/app/static/js/brew.js index ce15b27..d58005f 100755 --- a/app/static/js/brew.js +++ b/app/static/js/brew.js @@ -215,16 +215,36 @@ if (window.navigator && window.navigator.vibrate) window.navigator.vibrate([50, 100, 150]); // API Completion Call + let newItem = null; try { const res = await fetch('/api/brew/complete', { method: 'POST' }); const data = await res.json(); - if (data.new_item && window.notifyNewItem) { - window.notifyNewItem(data.new_item); - } + if (data.new_item) newItem = data.new_item; } catch (e) { console.error("Brew complete error:", e); } + if (!newItem) { + newItem = { + id: "felix_felicis", + name: "Felix Felicis", + icon: "🧪", + desc: "Ein Tropfen flüssiges Gold! Verleiht dir unerschütterliches Glück und enthüllt verborgene Runen." + }; + } + if (successPanel) successPanel.style.display = 'block'; + + if (window.showChamberVictory) { + window.showChamberVictory({ + chamberBadge: "Kammer III: Die Brauküche", + title: "⚗️ Zaubertrank Gemeistert!", + statsHtml: `🧪 Elixier: Felix Felicis🌡️ Hitze: Perfekt gemeistert`, + item: newItem, + storyText: "Der Kessel erstrahlt in gleißendem, flüssigem Gold! Du füllst eine Phiole des unbezahlbaren Felix Felicis ab. Seine Wärme durchdringt deinen Geist. Mit geschärften Sinnen trittst du vor das gewaltige Runen-Drehschloss der vier Gründer!", + nextUrl: "/cryptex", + nextBtnText: "➔ Weiter zu Kammer IV: Das Drehschloss 🔐" + }); + } } })(); diff --git a/app/static/js/broom.js b/app/static/js/broom.js index c14b21b..e84c067 100755 --- a/app/static/js/broom.js +++ b/app/static/js/broom.js @@ -35,7 +35,10 @@ window.startFlight = function() { const overlay = document.getElementById('broomStartOverlay'); - if (overlay) overlay.style.display = 'none'; + if (overlay) { + overlay.classList.add('hidden'); + overlay.style.display = 'none'; + } rings.length = 0; rings.push({ x: width + 60, y: height * 0.4, radius: 25, speed: 3.5, golden: false, collected: false, scale: 1, alpha: 1, pulse: 0 }); @@ -57,6 +60,20 @@ if (window.navigator && window.navigator.vibrate) window.navigator.vibrate(40); }; + // Explicitly bind click and pointerdown to start button for instant mobile activation + const startFlightBtn = document.getElementById('startFlightBtn'); + if (startFlightBtn) { + const onStartTrigger = (e) => { + if (e) { + e.preventDefault(); + e.stopPropagation(); + } + window.startFlight(); + }; + startFlightBtn.addEventListener('click', onStartTrigger); + startFlightBtn.addEventListener('pointerdown', onStartTrigger); + } + // Player (Broomstick) const player = { x: 90, @@ -505,7 +522,7 @@ requestAnimationFrame(loop); async function onCourseWin(elapsedSec) { - if (window.soundFX) window.soundFX.playMagicSuccess(); + let newItem = null; try { const res = await fetch('/api/broom/complete', { method: 'POST', @@ -513,23 +530,36 @@ body: JSON.stringify({ score: score, time_taken: elapsedSec }) }); const data = await res.json(); - if (data.new_item && window.notifyNewItem) { - window.notifyNewItem(data.new_item); - } + if (data.new_item) newItem = data.new_item; } catch (e) { console.error(e); } - 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'); + if (!newItem) { + newItem = { + id: "winged_key", + name: "Geflügelter Schlüssel", + icon: "🗝️", + desc: "Ein surrender Schlüssel mit Libellenflügeln. Er öffnet die Tür zur Duellier-Kammer!" + }; + } + + if (window.showChamberVictory) { + window.showChamberVictory({ + chamberBadge: "Kammer I: Ring-Flug", + title: "🏆 Ring-Parcours Gemeistert!", + statsHtml: `⏱️ Flugzeit: ${elapsedSec}s✨ Punkte: ${score} Pkt⭕ Ringe: ${TOTAL_RINGS_NEEDED}/${TOTAL_RINGS_NEEDED}`, + item: newItem, + storyText: "Mit einem meisterhaften Sturzflug fängst du den surrenden geflügelten Schlüssel! Madam Hoochs Silberpfeife ertönt anerkennend über dem Schloss. Die massive Eichentür zur verzauberten Duellier-Kammer öffnet sich!", + nextUrl: "/duel", + nextBtnText: "➔ Weiter zu Kammer II: Spruch-Duell 🪄" + }); + } else { + modalTitle.innerText = "🏆 Ring-Parcours Gemeistert!"; + modalText.innerHTML = `Mit atemberaubenden Manövern hast du alle ${TOTAL_RINGS_NEEDED} goldenen Ringe durchflogen!
Flugzeit: ${elapsedSec}s · Punkte: ${score}`; + modalBtn.innerText = "Weiter zum Zauberspruch-Duell 🪄"; + modalBtn.onclick = () => { window.location.href = '/duel'; }; + modalBackdrop.classList.add('show'); + } } })(); diff --git a/app/static/js/cryptex.js b/app/static/js/cryptex.js index ee2a0c8..d979489 100755 --- a/app/static/js/cryptex.js +++ b/app/static/js/cryptex.js @@ -146,9 +146,24 @@ window.notifyNewItem(data.new_item); } - setTimeout(() => { - if (successPanel) successPanel.style.display = 'block'; - }, 600); + if (successPanel) successPanel.style.display = 'block'; + + if (window.showChamberVictory) { + window.showChamberVictory({ + chamberBadge: "Kammer IV: Das Runen-Drehschloss", + title: "🔐 Gründer-Siegel Entriegelt!", + statsHtml: `🦁 Löwe🌙 Mond🔥 Feuer🪄 Zauberstab`, + item: data.new_item || { + id: "cryptex_key", + name: "Schlüssel der Gründer", + icon: "🗝️", + desc: "Ein uralter Messingschlüssel. Er sperrt die Pforte zur Verteidigungs-Kammer auf!" + }, + storyText: "Mit einem satten Grollen rasten die vier massiven Messingbolzen ein! Das Tor schnappt auf. Der Schlüssel der Gründer und Snapes Logik-Rolle wandern in deinen Beutel. Dahinter hörst du lautes Geschrei: Wichtel-Panik!", + nextUrl: "/pixies", + nextBtnText: "➔ Weiter zu Kammer V: Wichtel-Panik 🧚" + }); + } } else { if (window.soundFX) window.soundFX.playWhoosh(); @@ -174,4 +189,17 @@ console.error("Cryptex check error:", e); } }; + + // Felix Felicis Lucky Intuition Action + window.useFelixFelicisHint = function() { + if (window.soundFX) window.soundFX.playMagicSuccess(); + if (window.showFloatingToast) { + window.showFloatingToast("🍀 Felix Felicis wirkt! Eine goldene Stimme flüstert: »Löwe 🦁 und Mond 🌙 weisen den Weg!«"); + } + // Guide dials 0 and 1 + dials[0] = 0; // 🦁 + dials[1] = 1; // 🌙 + updateDialDisplay(0); + updateDialDisplay(1); + }; })(); diff --git a/app/static/js/duel.js b/app/static/js/duel.js index 177841b..013b364 100755 --- a/app/static/js/duel.js +++ b/app/static/js/duel.js @@ -302,27 +302,41 @@ } async function onDuelWon() { - if (window.soundFX) window.soundFX.playMagicSuccess(); + let newItem = null; 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); - } + if (data.new_item) newItem = 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 🗝️

- Die alte Rüstung verneigt sich ehrfürchtig. Das Tor öffnet sich und gibt den Weg zur dampfenden Zaubertrank-Brauküche frei!

- 🎒 Das goldene Duellanten-Abzeichen wurde deinem Zauberbeutel hinzugefügt! - `; - modalBtn.innerText = "Weiter zur Zaubertrank-Brauküche ⚗️ ➔"; - modalBtn.onclick = () => { window.location.href = '/brew'; }; - modalBackdrop.classList.add('show'); + if (!newItem) { + newItem = { + id: "duel_badge", + name: "Duellanten-Abzeichen & Kessel-Rezept", + icon: "⚡", + desc: "Ein goldenes Abzeichen und Snapes geheime Zaubertrank-Formel für die Brauküche." + }; + } + + if (window.showChamberVictory) { + window.showChamberVictory({ + chamberBadge: "Kammer II: Das Spruch-Duell", + title: "⚡ Duell Gemeistert!", + statsHtml: `🪄 Gesten: 4 Sprüche🛡️ Rüstung: Bezwungen!`, + item: newItem, + storyText: "Die verzauberte Rüstung bricht klirrend in sich zusammen! In ihrem Brustharnisch liegt ein versiegeltes Pergament: Snapes geheime Rezeptur für die Kessel-Brauküche. Das schwere Fallgitter zur Zaubertrank-Brauküche hebt sich!", + nextUrl: "/brew", + nextBtnText: "➔ Weiter zu Kammer III: Brauküche ⚗️" + }); + } else { + modalTitle.innerText = "⚡ Duell Gemeistert!"; + modalText.innerHTML = `Mit meisterhafter Zauberstabführung hast du alle Sprüche entfesselt und die Rüstung bezwungen!`; + modalBtn.innerText = "Weiter zur Brauküche ⚗️"; + modalBtn.onclick = () => { window.location.href = '/brew'; }; + modalBackdrop.classList.add('show'); + } } function render() { diff --git a/app/static/js/inventory.js b/app/static/js/inventory.js index 81e2111..7474235 100755 --- a/app/static/js/inventory.js +++ b/app/static/js/inventory.js @@ -230,6 +230,127 @@ window.showFloatingToast(`🎒 Neuer Gegenstand im Beutel: ${item.icon} ${item.name}!`); }; + // Confetti Animation Controller + let confettiAnimId = null; + function runVictoryConfetti() { + const canvas = document.getElementById('victoryConfettiCanvas'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + + const colors = ['#ffd700', '#f39c12', '#e74c3c', '#3498db', '#2ecc71', '#9b59b6', '#ffffff']; + const particles = []; + for (let i = 0; i < 90; i++) { + particles.push({ + x: Math.random() * canvas.width, + y: Math.random() * -canvas.height * 0.5, + w: Math.random() * 9 + 5, + h: Math.random() * 5 + 3, + vx: (Math.random() - 0.5) * 4, + vy: Math.random() * 3 + 2.5, + color: colors[Math.floor(Math.random() * colors.length)], + angle: Math.random() * Math.PI * 2, + vAngle: (Math.random() - 0.5) * 0.15, + shape: Math.random() > 0.4 ? 'rect' : 'star' + }); + } + + let start = Date.now(); + if (confettiAnimId) cancelAnimationFrame(confettiAnimId); + + function drawConfetti() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + const elapsed = (Date.now() - start) / 1000; + + particles.forEach(p => { + p.x += p.vx; + p.y += p.vy; + p.angle += p.vAngle; + + ctx.save(); + ctx.translate(p.x, p.y); + ctx.rotate(p.angle); + ctx.fillStyle = p.color; + + if (p.shape === 'star') { + ctx.beginPath(); + ctx.arc(0, 0, p.w * 0.45, 0, Math.PI * 2); + ctx.fill(); + } else { + ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); + } + ctx.restore(); + + if (p.y > canvas.height + 20) { + p.y = -20; + p.x = Math.random() * canvas.width; + } + }); + + if (elapsed < 8) { + confettiAnimId = requestAnimationFrame(drawConfetti); + } else { + ctx.clearRect(0, 0, canvas.width, canvas.height); + } + } + confettiAnimId = requestAnimationFrame(drawConfetti); + } + + // Unified Spectacular Victory Celebration Overlay + window.showChamberVictory = function(config) { + const modal = document.getElementById('victoryCelebrationModal'); + if (!modal) { + // Fallback to standard modal if celebration overlay missing + if (window.modalTitle) window.modalTitle.innerText = config.title; + if (window.modalText) window.modalText.innerHTML = config.storyText; + if (window.modalBackdrop) window.modalBackdrop.classList.add('show'); + return; + } + + const badge = document.getElementById('vicChamberBadge'); + const title = document.getElementById('vicTitle'); + const statsBox = document.getElementById('vicStatsBox'); + const itemBox = document.getElementById('vicItemRewardBox'); + const rewardIcon = document.getElementById('vicRewardIcon'); + const rewardName = document.getElementById('vicRewardName'); + const rewardDesc = document.getElementById('vicRewardDesc'); + const storyBox = document.getElementById('vicStoryBox'); + const nextBtn = document.getElementById('vicNextBtn'); + + if (badge) badge.innerText = config.chamberBadge || 'Kammer Gemeistert'; + if (title) title.innerText = config.title || 'Hervorragend!'; + if (statsBox) { + statsBox.innerHTML = config.statsHtml || ''; + statsBox.style.display = config.statsHtml ? 'flex' : 'none'; + } + if (storyBox) storyBox.innerHTML = config.storyText || ''; + + if (config.item && itemBox) { + itemBox.style.display = 'flex'; + if (rewardIcon) rewardIcon.innerText = config.item.icon || '✨'; + if (rewardName) rewardName.innerText = config.item.name || ''; + if (rewardDesc) rewardDesc.innerText = config.item.desc || ''; + window.notifyNewItem(config.item); + } else if (itemBox) { + itemBox.style.display = 'none'; + } + + if (nextBtn) { + nextBtn.href = config.nextUrl || '/'; + nextBtn.innerHTML = config.nextBtnText || '➔ Weiter zur nächsten Kammer ✨'; + } + + // Trigger Audio, Haptic & Particle Celebration + if (window.soundFX) window.soundFX.playMagicSuccess(); + if (window.navigator && window.navigator.vibrate) { + window.navigator.vibrate([60, 50, 80, 50, 180]); + } + + modal.classList.add('show'); + runVictoryConfetti(); + }; + // Auto-select first item if available if (itemSlots.length > 0) { selectItem(itemSlots[0]); diff --git a/app/static/js/pixies.js b/app/static/js/pixies.js index 6b0ddce..4eef9b4 100755 --- a/app/static/js/pixies.js +++ b/app/static/js/pixies.js @@ -165,11 +165,7 @@ activeTargets.clear(); if (won) { - if (window.soundFX) window.soundFX.playMagicSuccess(); - if (window.navigator && window.navigator.vibrate) { - window.navigator.vibrate([50, 100, 150]); - } - + let newItem = null; try { const res = await fetch('/api/pixies/complete', { method: 'POST', @@ -177,15 +173,34 @@ body: JSON.stringify({ caught: score }) }); const data = await res.json(); - if (data.new_item && window.notifyNewItem) { - window.notifyNewItem(data.new_item); - } + if (data.new_item) newItem = data.new_item; } catch (e) { console.error("Pixies complete error:", e); } + if (!newItem) { + newItem = { + id: "pixie_jar", + name: "Wichtelglas", + icon: "🧚", + desc: "Ein dickbauchiges Glas mit einem gebannten Wichtel." + }; + } + if (successPanel) successPanel.style.display = 'block'; + if (window.showChamberVictory) { + window.showChamberVictory({ + chamberBadge: "Kammer V: Wichtel-Panik", + title: "🧚 Wichtel Gebannt!", + statsHtml: `❄️ Gebannt: ${score} Wichtel👦 Neville: Gerettet`, + item: newItem, + storyText: "Mit blitzschnellen Immobilus!-Zaubern hast du alle 10 Wichtel eingefroren! Ein zappelnder Wichtel landet sicher im Wichtelglas. Neville bedankt sich erleichtert. Der Weg zur berüchtigten Flammenkammer von Severus Snape liegt nun offen!", + nextUrl: "/bottles", + nextBtnText: "➔ Weiter zu Kammer VI: Snapes Flaschen 🧪" + }); + } + } else { if (window.soundFX) window.soundFX.playWhoosh(); if (startOverlay) { diff --git a/app/static/js/sliding.js b/app/static/js/sliding.js index 79216e9..71ac1f4 100755 --- a/app/static/js/sliding.js +++ b/app/static/js/sliding.js @@ -125,14 +125,25 @@ fetch('/api/sliding/complete', { method: 'POST' }).catch(console.error); - modalTitle.innerText = "✨ Schieberätsel gelöst!"; - modalText.innerHTML = `Mit geschicktem Schieben hast du alle 8 alchemistischen Phiolen in die perfekte harmonische Ordnung gebracht!

- Züge: ${moves}
- Zeit: ${secondsElapsed} Sekunden

- Die Runenplatte leuchtet golden auf und der Weg zur Flammenkammer öffnet sich!`; - modalBtn.innerText = "Zur Flammenkammer 🧪"; - modalBtn.onclick = () => { window.location.href = '/bottles'; }; - modalBackdrop.classList.add('show'); + if (window.showChamberVictory) { + window.showChamberVictory({ + chamberBadge: "Alchemie-Zusatzprüfung", + title: "✨ Schiebesiegel Gelöst!", + statsHtml: `⚗️ Züge: ${moves}⏱️ Zeit: ${secondsElapsed}s`, + storyText: "Mit geschicktem Schieben hast du alle 8 alchemistischen Essenzen in harmonische Ordnung gebracht! Das antike Relief leuchtet golden auf.", + nextUrl: "/bottles", + nextBtnText: "➔ Weiter zu Snapes Flaschen 🧪" + }); + } else { + modalTitle.innerText = "✨ Schieberätsel gelöst!"; + modalText.innerHTML = `Mit geschicktem Schieben hast du alle 8 alchemistischen Phiolen in die perfekte harmonische Ordnung gebracht!

+ Züge: ${moves}
+ Zeit: ${secondsElapsed} Sekunden

+ Die Runenplatte leuchtet golden auf und der Weg zur Flammenkammer öffnet sich!`; + modalBtn.innerText = "Zur Flammenkammer 🧪"; + modalBtn.onclick = () => { window.location.href = '/bottles'; }; + modalBackdrop.classList.add('show'); + } } } diff --git a/app/templates/base.html b/app/templates/base.html index 72c2322..369097a 100755 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -39,7 +39,7 @@ {% block content %}{% endblock %} - + + +
+ +
+
+
🏆
+
+ +
Kammer Gemeistert
+

Hervorragend, Zauberschüler!

+ +
+ +
+ + + +
+ +
+ + +
+
+
diff --git a/app/templates/bottles.html b/app/templates/bottles.html index 22b30a0..685ad3c 100755 --- a/app/templates/bottles.html +++ b/app/templates/bottles.html @@ -1,10 +1,10 @@ {% extends "base.html" %} -{% block title %}Kammer II: Snapes Flaschenrätsel & Flammenkammer · Hogwarts Escape{% endblock %} +{% block title %}Kammer VI: Snapes Flaschenrätsel & Flammenkammer · Hogwarts Escape{% endblock %} {% block content %}
-

🧪 Kammer II: Snapes Flaschenrätsel 🔥

+

🧪 Kammer VI: Snapes Flaschenrätsel & Flammenkammer 🔥

"Gefahr liegt vor euch, die Rettung zurück, @@ -28,6 +28,31 @@
+{% if is_locked and lock_info %} +
+
🔒
+

Kammer Versiegelt!

+

+ {{ lock_info.reason }} +

+
+
{{ lock_info.item_icon }}
+
+ {{ lock_info.item_name }} + Erforderlich aus: {{ lock_info.from_chamber }} +
+
+ +
+{% endif %} +
diff --git a/app/templates/brew.html b/app/templates/brew.html index 46987eb..28689f8 100755 --- a/app/templates/brew.html +++ b/app/templates/brew.html @@ -11,6 +11,31 @@

+{% if is_locked and lock_info %} +
+
🔒
+

Kammer Versiegelt!

+

+ {{ lock_info.reason }} +

+
+
{{ lock_info.item_icon }}
+
+ {{ lock_info.item_name }} + Erforderlich aus: {{ lock_info.from_chamber }} +
+
+ +
+{% endif %} +
diff --git a/app/templates/cryptex.html b/app/templates/cryptex.html index f3e4c08..0cbba1c 100755 --- a/app/templates/cryptex.html +++ b/app/templates/cryptex.html @@ -19,8 +19,40 @@ Nicht das Schloss selbst, nicht das alte Pergament, nicht die Glasphiole – welches Instrument aus Holz und Phönixfeder schlägt die Funken?«
+ + +
+ +
+{% if is_locked and lock_info %} +
+
🔒
+

Kammer Versiegelt!

+

+ {{ lock_info.reason }} +

+
+
{{ lock_info.item_icon }}
+
+ {{ lock_info.item_name }} + Erforderlich aus: {{ lock_info.from_chamber }} +
+
+ +
+{% endif %} +
+{% if is_locked and lock_info %} +
+
🔒
+

Kammer Versiegelt!

+

+ {{ lock_info.reason }} +

+
+
{{ lock_info.item_icon }}
+
+ {{ lock_info.item_name }} + Erforderlich aus: {{ lock_info.from_chamber }} +
+
+ +
+{% endif %} +
🪄 Spruch: Flipendo ⚡
diff --git a/app/templates/index.html b/app/templates/index.html index 95b44d8..fcf3957 100755 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -24,13 +24,13 @@
- {% if state.broom_solved %}✅ Gemeistert{% else %}🔓 Bereit{% endif %} + {% if state.broom_solved %}✅ Gemeistert{% else %}🔓 Bereit (Start){% endif %}
🧹⭕

Kammer I: Der Ring-Flug

Madam Hoochs Quidditch-Parcours! Schwinge dich auf deinen Rennbesen, steuere geschmeidig durch - die schwebenden goldenen Ringe und sammle Punkte und Speed-Combos! + die schwebenden goldenen Ringe und fange den geflügelten Schlüssel!

Parcours fliegen 🧹
@@ -38,7 +38,7 @@
- {% if state.duel_solved %}✅ Gemeistert{% else %}⚡ Bereit{% endif %} + {% if state.duel_solved %}✅ Gemeistert{% elif 'winged_key' in state.inventory or state.broom_solved or state.alohomora_mode %}⚡ Bereit{% else %}🔒 Benötigt: Geflügelter Schlüssel{% endif %}
🪄⚡

Kammer II: Das Spruch-Duell

@@ -52,12 +52,12 @@
- {% if state.brew_solved %}✅ Gemeistert{% else %}⚗️ Bereit{% endif %} + {% if state.brew_solved %}✅ Gemeistert{% elif 'duel_badge' in state.inventory or state.duel_solved or state.alohomora_mode %}⚗️ Bereit{% else %}🔒 Benötigt: Kessel-Rezeptur{% endif %}
⚗️🧪

Kammer III: Die Brauküche

- Snapes Kupferkessel brodelt! Gib Drachenblut und Sternenstaub hinzu, rühre mit kreisenden Gesten + Snapes Kupferkessel brodelt! Gib Essenzen hinzu, rühre mit Bedacht und balanciere die Hitze, um das goldene Glückselixier Felix Felicis zu vollenden!

Kessel anheizen ⚗️ @@ -66,7 +66,7 @@
- {% if state.cryptex_solved %}✅ Gemeistert{% else %}🔐 Bereit{% endif %} + {% if state.cryptex_solved %}✅ Gemeistert{% elif 'felix_felicis' in state.inventory or state.brew_solved or state.alohomora_mode %}🔐 Bereit{% else %}🔒 Benötigt: Felix Felicis{% endif %}
🔐🗝️

Kammer IV: Das Drehschloss

@@ -80,7 +80,7 @@
- {% if state.pixies_solved %}✅ Gemeistert{% else %}🧚 Bereit{% endif %} + {% if state.pixies_solved %}✅ Gemeistert{% elif 'cryptex_key' in state.inventory or state.cryptex_solved or state.alohomora_mode %}🧚 Bereit{% else %}🔒 Benötigt: Gründerschlüssel{% endif %}
🧚❄️

Kammer V: Wichtel-Panik

@@ -94,7 +94,7 @@
- {% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Bereit{% endif %} + {% if state.bottles_solved %}✅ Gemeistert{% elif 'pixie_jar' in state.inventory or state.pixies_solved or state.alohomora_mode %}🔥 Bereit{% else %}🔒 Benötigt: Wichtelglas{% endif %}
🧪🔥

Kammer VI: Snapes Flaschen

diff --git a/app/templates/pixies.html b/app/templates/pixies.html index 3283800..957cd7c 100755 --- a/app/templates/pixies.html +++ b/app/templates/pixies.html @@ -11,6 +11,31 @@

+{% if is_locked and lock_info %} +
+
🔒
+

Kammer Versiegelt!

+

+ {{ lock_info.reason }} +

+
+
{{ lock_info.item_icon }}
+
+ {{ lock_info.item_name }} + Erforderlich aus: {{ lock_info.from_chamber }} +
+
+ +
+{% endif %} +
diff --git a/test_app.py b/test_app.py index 2c436fb..a395d91 100755 --- a/test_app.py +++ b/test_app.py @@ -66,10 +66,11 @@ def test_endpoints(): assert res.status_code == 200 assert res.json()["house"]["name"] == "Ravenclaw" - # Test API Broom complete + # Test API Broom complete (adds winged_key) res = client.post("/api/broom/complete", json={"score": 850, "time_taken": 22}) assert res.status_code == 200 assert res.json()["success"] is True + assert res.json()["new_item"]["id"] == "winged_key" # Test API Duel complete (adds duel_badge) res = client.post("/api/duel/complete")