diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc index f6fc3f0..8f8c267 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 e7f3ba4..8f7e5f2 100755 --- a/app/main.py +++ b/app/main.py @@ -56,6 +56,30 @@ ALL_ITEMS = { "action": "spark_wand", "action_label": "Funkenregen sprühen ⚡" }, + "felix_felicis": { + "id": "felix_felicis", + "name": "Felix Felicis", + "icon": "🧪", + "desc": "Ein Tropfen flüssiges Gold! Verleiht dir unerschütterliches Glück bei deinen magischen Taten.", + "action": "lucky_aura", + "action_label": "Glücks-Aura aktivieren 🍀" + }, + "cryptex_key": { + "id": "cryptex_key", + "name": "Schlüssel der Gründer", + "icon": "🗝️", + "desc": "Ein schwerer, mit uralten Runen gravierter Messingschlüssel aus dem Tresor von Hogwarts.", + "action": "inspect_key", + "action_label": "Runen betrachten 🔍" + }, + "pixie_jar": { + "id": "pixie_jar", + "name": "Wichtelglas", + "icon": "🧚", + "desc": "Ein dickbauchiges Glas mit einem gebannten blauen Wichtel. Er schneidet lustige Fratzen!", + "action": "poke_jar", + "action_label": "Glas antippen ⚡" + }, "snape_scroll": { "id": "snape_scroll", "name": "Snapes Logik-Rolle", @@ -81,6 +105,8 @@ HOUSES = { "hufflepuff": {"id": "hufflepuff", "name": "Hufflepuff", "icon": "🦡", "color": "#ecb939", "accent": "#372e29"} } +CRYPTEX_SOLUTION = ["🦁", "🌙", "🔥", "🪄"] + def get_session_state(request: Request) -> dict: if "escape" not in request.session: request.session["escape"] = { @@ -91,6 +117,9 @@ def get_session_state(request: Request) -> dict: "broom_score": 0, "broom_time": 0, "duel_solved": False, + "brew_solved": False, + "cryptex_solved": False, + "pixies_solved": False, "bottles_solved": False, "sliding_solved": False, "started_at": datetime.datetime.utcnow().isoformat(), @@ -101,14 +130,26 @@ 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("duel_solved"): + if state.get("duel_solved") and "duel_badge" not in state["inventory"]: state["inventory"].append("duel_badge") - if state.get("bottles_solved"): + if state.get("brew_solved") and "felix_felicis" not in state["inventory"]: + state["inventory"].append("felix_felicis") + if state.get("cryptex_solved") and "cryptex_key" not in state["inventory"]: + state["inventory"].append("cryptex_key") + if state.get("pixies_solved") and "pixie_jar" not in state["inventory"]: + state["inventory"].append("pixie_jar") + if state.get("bottles_solved") and "philosophers_stone" not in state["inventory"]: state["inventory"].append("philosophers_stone") if "house" not in state: state["house"] = "gryffindor" if "duel_solved" not in state: state["duel_solved"] = False + if "brew_solved" not in state: + state["brew_solved"] = False + if "cryptex_solved" not in state: + state["cryptex_solved"] = False + if "pixies_solved" not in state: + state["pixies_solved"] = False request.session["escape"] = state return request.session["escape"] @@ -161,6 +202,42 @@ async def page_duel(request: Request): context=get_template_context(request, state, lang) ) +@app.get("/brew", response_class=HTMLResponse) +async def page_brew(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="brew.html", + context=get_template_context(request, state, lang) + ) + +@app.get("/cryptex", response_class=HTMLResponse) +async def page_cryptex(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="cryptex.html", + context=get_template_context(request, state, lang) + ) + +@app.get("/pixies", response_class=HTMLResponse) +async def page_pixies(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="pixies.html", + context=get_template_context(request, state, lang) + ) + @app.get("/chess") async def redirect_chess(): # Graceful redirect for old links @@ -219,6 +296,9 @@ async def api_get_inventory(request: Request): "score": state.get("broom_score", 0), "broom_solved": state.get("broom_solved", False), "duel_solved": state.get("duel_solved", False), + "brew_solved": state.get("brew_solved", False), + "cryptex_solved": state.get("cryptex_solved", False), + "pixies_solved": state.get("pixies_solved", False), "bottles_solved": state.get("bottles_solved", False), "sliding_solved": state.get("sliding_solved", False) }) @@ -247,6 +327,9 @@ async def api_reset_state(request: Request): "broom_score": 0, "broom_time": 0, "duel_solved": False, + "brew_solved": False, + "cryptex_solved": False, + "pixies_solved": False, "bottles_solved": False, "sliding_solved": False, "started_at": datetime.datetime.utcnow().isoformat(), @@ -283,11 +366,90 @@ async def api_duel_complete(request: Request): request.session["escape"] = state return JSONResponse({ "success": True, - "message": "Zauberspruch-Duell gewonnen! Die Pforte zur Flammenkammer steht weit offen!", + "message": "Zauberspruch-Duell gewonnen! Die Pforte zur Brauküche steht weit offen!", "new_item": ALL_ITEMS["duel_badge"], "state": state }) +@app.post("/api/brew/complete") +async def api_brew_complete(request: Request): + state = get_session_state(request) + state["brew_solved"] = True + if "inventory" not in state: + state["inventory"] = ["wand", "broom", "snape_scroll"] + if "felix_felicis" not in state["inventory"]: + state["inventory"].append("felix_felicis") + request.session["escape"] = state + return JSONResponse({ + "success": True, + "message": "Goldener Glanz erfüllt den Kessel! Felix Felicis ist meisterhaft gebraut.", + "new_item": ALL_ITEMS["felix_felicis"], + "state": state + }) + +@app.post("/api/cryptex/unlock") +async def api_cryptex_unlock(request: Request): + try: + data = await request.json() + except Exception: + data = {} + combo = data.get("combination", []) + + # Check match against CRYPTEX_SOLUTION ["🦁", "🌙", "🔥", "🪄"] + is_correct = False + if combo == CRYPTEX_SOLUTION: + is_correct = True + elif combo == [0, 1, 0, 1]: + is_correct = True + elif combo == ["lion", "moon", "fire", "wand"]: + is_correct = True + + if not is_correct: + return JSONResponse({ + "success": False, + "message": "Die Runenzylinder rasten nicht ein. Die Gravur bleibt verschlossen!" + }) + + state = get_session_state(request) + state["cryptex_solved"] = True + if "inventory" not in state: + state["inventory"] = ["wand", "broom", "snape_scroll"] + if "cryptex_key" not in state["inventory"]: + state["inventory"].append("cryptex_key") + request.session["escape"] = state + return JSONResponse({ + "success": True, + "message": "Das Runenschloss springt mit einem goldenen Glühen auf! Der Schlüssel der Gründer gehört dir.", + "new_item": ALL_ITEMS["cryptex_key"], + "state": state + }) + +@app.post("/api/pixies/complete") +async def api_pixies_complete(request: Request): + try: + data = await request.json() + except Exception: + data = {} + caught = int(data.get("caught", 0)) + if caught < 10: + return JSONResponse({ + "success": False, + "message": f"Du hast erst {caught}/10 Wichtel gebannt. Halte durch!" + }) + state = get_session_state(request) + state["pixies_solved"] = True + if "inventory" not in state: + state["inventory"] = ["wand", "broom", "snape_scroll"] + if "pixie_jar" not in state["inventory"]: + state["inventory"].append("pixie_jar") + request.session["escape"] = state + return JSONResponse({ + "success": True, + "message": "Alle frechen Wichtel mit Immobilus gebannt! Das Wichtelglas wandert in deinen Beutel.", + "new_item": ALL_ITEMS["pixie_jar"], + "state": state + }) + @app.post("/api/bottles/drink") async def api_bottles_drink(request: Request): try: diff --git a/app/static/css/escape.css b/app/static/css/escape.css index c71a7ce..3759464 100755 --- a/app/static/css/escape.css +++ b/app/static/css/escape.css @@ -1190,3 +1190,495 @@ body { } } +/* ======================================================== + MINI-GAME: DIE ZAUBERTRANK-BRAUKÜCHE (/brew) + ======================================================== */ +.brew-container { + max-width: 650px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.25rem; +} + +.recipe-book { + background: linear-gradient(135deg, #2b1f14, #19120b); + border: 2px solid var(--gold); + border-radius: 12px; + padding: 1rem 1.25rem; + width: 100%; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.7); + text-align: center; +} + +.recipe-step-badge { + display: inline-block; + background: rgba(212, 175, 55, 0.2); + border: 1px solid var(--gold); + color: var(--gold); + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.85rem; + font-weight: bold; + margin-bottom: 0.5rem; +} + +.cauldron-wrapper { + position: relative; + width: 280px; + height: 280px; + display: flex; + justify-content: center; + align-items: center; + margin: 0.5rem 0; + touch-action: none; +} + +.cauldron-pot { + width: 250px; + height: 230px; + background: radial-gradient(circle at 35% 30%, #4a4a55 0%, #202025 50%, #0d0d12 100%); + border-radius: 50% 50% 45% 45% / 30% 30% 60% 60%; + border: 6px solid #1a1a20; + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.9), inset 0 -10px 20px rgba(0, 0, 0, 0.8); + position: relative; + display: flex; + justify-content: center; + align-items: flex-start; + padding-top: 25px; +} + +.cauldron-pot::before { + content: ''; + position: absolute; + top: 5px; + width: 210px; + height: 48px; + border-radius: 50%; + background: #111116; + border: 4px solid #2d2d38; +} + +.cauldron-pot::after { + content: ''; + position: absolute; + bottom: -16px; + width: 120px; + height: 18px; + background: #000; + border-radius: 50%; + filter: blur(8px); +} + +.potion-surface { + position: absolute; + top: 12px; + width: 190px; + height: 36px; + border-radius: 50%; + background: radial-gradient(ellipse at center, #2ecc71 0%, #16a085 60%, #0e5042 100%); + box-shadow: 0 0 20px #2ecc71; + overflow: hidden; + transition: background 1s ease, box-shadow 1s ease; +} + +.potion-surface.state-stir { + background: radial-gradient(ellipse at center, #9b59b6 0%, #8e44ad 60%, #4a235a 100%); + box-shadow: 0 0 25px #9b59b6; +} + +.potion-surface.state-heat { + background: radial-gradient(ellipse at center, #e67e22 0%, #d35400 60%, #78281f 100%); + box-shadow: 0 0 25px #e67e22; +} + +.potion-surface.state-gold { + background: radial-gradient(ellipse at center, #fff6a5 0%, #f1c40f 50%, #b7950b 100%); + box-shadow: 0 0 35px #f1c40f, 0 0 60px rgba(241, 196, 15, 0.6); + animation: goldShimmer 1.5s infinite alternate; +} + +@keyframes goldShimmer { + 0% { filter: brightness(1) drop-shadow(0 0 10px #f1c40f); } + 100% { filter: brightness(1.3) drop-shadow(0 0 30px #fff); } +} + +.cauldron-fire { + position: absolute; + bottom: 5px; + font-size: 2.2rem; + animation: flameFlicker 0.8s infinite alternate; + pointer-events: none; +} + +/* Stir Interactive Ring */ +.stir-circle { + position: absolute; + width: 180px; + height: 180px; + border-radius: 50%; + border: 3px dashed var(--gold); + display: none; + align-items: center; + justify-content: center; + pointer-events: auto; + cursor: grab; + animation: rotateDashed 6s linear infinite; +} + +.stir-circle.active { + display: flex; +} + +.stir-arrow-hint { + font-size: 1.8rem; + position: absolute; + top: 5px; + right: 35px; +} + +@keyframes rotateDashed { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Ingredient Shelf */ +.ingredients-shelf { + display: flex; + gap: 0.75rem; + justify-content: center; + flex-wrap: wrap; + width: 100%; +} + +.ingredient-btn { + background: rgba(30, 24, 40, 0.85); + border: 2px solid #5a422d; + border-radius: 12px; + padding: 0.6rem 0.8rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + color: var(--text-light); + cursor: pointer; + transition: all 0.2s; + user-select: none; + min-width: 90px; +} + +.ingredient-btn:hover, .ingredient-btn:active { + border-color: var(--gold); + transform: translateY(-3px) scale(1.03); + box-shadow: 0 6px 16px rgba(212, 175, 55, 0.3); +} + +.ingredient-btn.done { + opacity: 0.5; + border-color: #27ae60; + pointer-events: none; +} + +.ing-icon { + font-size: 1.8rem; +} + +.ing-name { + font-size: 0.85rem; + font-weight: bold; +} + +.ing-count { + font-size: 0.75rem; + color: var(--gold); + background: rgba(0, 0, 0, 0.4); + padding: 0.1rem 0.5rem; + border-radius: 10px; +} + +/* Heat Bellows Bar */ +.heat-control-panel { + display: none; + flex-direction: column; + align-items: center; + width: 100%; + gap: 0.75rem; +} + +.heat-control-panel.active { + display: flex; +} + +.temp-bar-container { + width: 100%; + max-width: 320px; + height: 24px; + background: #111; + border: 2px solid #555; + border-radius: 12px; + position: relative; + overflow: hidden; +} + +.temp-ideal-zone { + position: absolute; + left: 60%; + width: 25%; + height: 100%; + background: rgba(46, 204, 113, 0.4); + border-left: 2px dashed #2ecc71; + border-right: 2px dashed #2ecc71; +} + +.temp-indicator { + height: 100%; + width: 10%; + background: linear-gradient(90deg, #3498db, #f39c12, #e74c3c); + border-radius: 10px; + transition: width 0.15s ease-out; +} + +.bellows-btn { + background: linear-gradient(180deg, #c0392b, #962d22); + border: 2px solid #e74c3c; + border-radius: 12px; + color: #fff; + padding: 0.75rem 1.5rem; + font-size: 1.1rem; + font-family: inherit; + font-weight: bold; + cursor: pointer; + box-shadow: 0 6px 18px rgba(231, 76, 60, 0.4); + display: inline-flex; + align-items: center; + gap: 0.5rem; + user-select: none; + touch-action: manipulation; +} + +.bellows-btn:active { + transform: scale(0.95); + background: #e74c3c; +} + + +/* ======================================================== + MINI-GAME: DAS RUNEN-DREHSCHLOSS / CRYPTEX (/cryptex) + ======================================================== */ +.cryptex-container { + max-width: 600px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.25rem; +} + +.cryptex-box { + background: linear-gradient(180deg, #3a2b1d 0%, #1f160e 50%, #120d08 100%); + border: 4px solid var(--gold); + border-radius: 16px; + padding: 1.25rem 1rem; + box-shadow: 0 12px 35px rgba(0, 0, 0, 0.9), inset 0 0 25px rgba(212, 175, 55, 0.15); + width: 100%; + position: relative; +} + +.cryptex-frame-line { + position: absolute; + top: 50%; + left: 4%; + right: 4%; + height: 4px; + background: rgba(212, 175, 55, 0.85); + box-shadow: 0 0 10px var(--gold); + transform: translateY(-50%); + pointer-events: none; + z-index: 10; +} + +.cryptex-rings-row { + display: flex; + justify-content: space-around; + gap: 0.5rem; + position: relative; + z-index: 5; +} + +.cryptex-dial { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + flex: 1; +} + +.dial-step-btn { + background: rgba(212, 175, 55, 0.15); + border: 1px solid var(--gold); + color: var(--gold); + border-radius: 8px; + width: 44px; + height: 38px; + font-size: 1.2rem; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + user-select: none; + touch-action: manipulation; + transition: all 0.15s; +} + +.dial-step-btn:active { + background: var(--gold); + color: #120d08; + transform: scale(0.92); +} + +.dial-cylinder { + width: 100%; + max-width: 90px; + height: 90px; + background: linear-gradient(180deg, #1b1712 0%, #4a3822 50%, #15110c 100%); + border: 3px solid #8c6e3b; + border-radius: 12px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + box-shadow: inset 0 8px 12px rgba(0,0,0,0.8), inset 0 -8px 12px rgba(0,0,0,0.8); + position: relative; + user-select: none; +} + +.dial-symbol { + font-size: 2.4rem; + filter: drop-shadow(0 2px 6px rgba(0,0,0,0.8)); + transition: transform 0.2s ease; +} + +.dial-label { + font-size: 0.72rem; + color: var(--gold-light); + margin-top: -2px; + font-weight: bold; +} + + +/* ======================================================== + MINI-GAME: WICHTEL-PANIK / CORNISH PIXIES (/pixies) + ======================================================== */ +.pixies-container { + max-width: 580px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.pixies-hud { + display: flex; + justify-content: space-between; + width: 100%; + background: rgba(20, 15, 30, 0.85); + border: 2px solid var(--gold); + border-radius: 12px; + padding: 0.6rem 1rem; + font-size: 1.1rem; + box-shadow: 0 4px 15px rgba(0,0,0,0.6); +} + +.pixies-arena { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; + width: 100%; + aspect-ratio: 1 / 1; + background: linear-gradient(135deg, #1f1728, #100b17); + border: 3px solid #4a3461; + border-radius: 16px; + padding: 0.75rem; + box-shadow: inset 0 0 25px rgba(0,0,0,0.8); + touch-action: manipulation; +} + +.pixie-hole { + background: radial-gradient(circle, #2b1f3d 0%, #150f20 80%); + border: 2px solid #3c2a50; + border-radius: 12px; + position: relative; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + user-select: none; + box-shadow: inset 0 6px 10px rgba(0,0,0,0.7); +} + +.hole-decor { + font-size: 1.5rem; + opacity: 0.25; + position: absolute; + bottom: 6px; + pointer-events: none; +} + +.pixie-target { + font-size: 3.2rem; + position: absolute; + transform: translateY(120%); + transition: transform 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28); + pointer-events: auto; + filter: drop-shadow(0 4px 8px rgba(0,0,0,0.8)); + animation: pixieFloat 0.8s infinite alternate ease-in-out; +} + +.pixie-target.active { + transform: translateY(0); +} + +.pixie-target.frozen { + filter: drop-shadow(0 0 12px #3498db) hue-rotate(180deg) brightness(1.2); + animation: none; + transform: translateY(0) scale(1.08); +} + +.pixie-target.neville { + font-size: 3.4rem; + animation: shakeNeville 0.4s infinite alternate; +} + +@keyframes pixieFloat { + 0% { transform: translateY(-4px) rotate(-3deg); } + 100% { transform: translateY(4px) rotate(3deg); } +} + +@keyframes shakeNeville { + 0% { transform: translateY(0) rotate(-5deg); } + 100% { transform: translateY(0) rotate(5deg); } +} + +@media (max-width: 480px) { + .cauldron-wrapper { + width: 240px; + height: 240px; + } + .cauldron-pot { + width: 215px; + height: 200px; + } + .dial-cylinder { + height: 75px; + } + .dial-symbol { + font-size: 2rem; + } + .pixie-target { + font-size: 2.6rem; + } +} + diff --git a/app/static/js/brew.js b/app/static/js/brew.js new file mode 100755 index 0000000..1b6951c --- /dev/null +++ b/app/static/js/brew.js @@ -0,0 +1,227 @@ +// Zaubertrank-Brauküche (Felix Felicis Potion Brewing Engine) +(function() { + let currentStep = 1; // 1: Ingredients, 2: Stirring, 3: Heat, 4: Done + const ingredients = { + dragon: 0, + mandrake: 0, + star: 0 + }; + const targetIngredients = { + dragon: 2, + mandrake: 1, + star: 1 + }; + + // DOM Elements + const stepBadge = document.getElementById('stepBadge'); + const stepInstruction = document.getElementById('stepInstruction'); + const stepSubtext = document.getElementById('stepSubtext'); + const ingredientsShelf = document.getElementById('ingredientsShelf'); + const potionSurface = document.getElementById('potionSurface'); + const stirCircle = document.getElementById('stirCircle'); + const stirTracker = document.getElementById('stirTracker'); + const stirCounterText = document.getElementById('stirCounterText'); + const stirCountSpan = document.getElementById('stirCount'); + const heatPanel = document.getElementById('heatPanel'); + const tempIndicator = document.getElementById('tempIndicator'); + const heatTimerSpan = document.getElementById('heatTimer'); + const successPanel = document.getElementById('successPanel'); + const cauldronZone = document.getElementById('cauldronZone'); + + // Step 1: Add Ingredient + window.addIngredient = function(type) { + if (currentStep !== 1) return; + if (ingredients[type] >= targetIngredients[type]) return; + + ingredients[type]++; + if (window.soundFX) window.soundFX.playBottleClink(); + + // Update UI Badge + if (type === 'dragon') { + document.getElementById('countDragon').innerText = `${ingredients.dragon} / ${targetIngredients.dragon}`; + if (ingredients.dragon >= targetIngredients.dragon) document.getElementById('ingDragon').classList.add('done'); + } else if (type === 'mandrake') { + document.getElementById('countMandrake').innerText = `${ingredients.mandrake} / ${targetIngredients.mandrake}`; + if (ingredients.mandrake >= targetIngredients.mandrake) document.getElementById('ingMandrake').classList.add('done'); + } else if (type === 'star') { + document.getElementById('countStar').innerText = `${ingredients.star} / ${targetIngredients.star}`; + if (ingredients.star >= targetIngredients.star) document.getElementById('ingStar').classList.add('done'); + } + + // Ripple effect on surface + potionSurface.style.transform = 'scale(1.08)'; + setTimeout(() => potionSurface.style.transform = 'scale(1)', 200); + + // Check if all ingredients added + const total = ingredients.dragon + ingredients.mandrake + ingredients.star; + if (total === 4) { + setTimeout(startStep2, 600); + } + }; + + // Step 2: Stirring + let completedStirs = 0; + let lastAngle = null; + let accumulatedAngle = 0; + + function startStep2() { + currentStep = 2; + stepBadge.innerText = "Schritt 2: Gleichmäßig rühren"; + stepInstruction.innerText = "Rühre den Sud 3 Runden im Uhrzeigersinn!"; + stepSubtext.innerText = "Führe deinen Finger oder die Maus im Kreis über dem Kessel oder tippe ihn an."; + ingredientsShelf.style.display = 'none'; + stirCounterText.style.display = 'block'; + stirCircle.classList.add('active'); + potionSurface.className = 'potion-surface state-stir'; + + if (window.soundFX) window.soundFX.playStoneSlide(); + } + + // Touch / Pointer stirring logic + let isStirring = false; + + function getAngle(e) { + const rect = cauldronZone.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const clientX = e.touches ? e.touches[0].clientX : e.clientX; + const clientY = e.touches ? e.touches[0].clientY : e.clientY; + return Math.atan2(clientY - centerY, clientX - centerX) * (180 / Math.PI); + } + + function onStirStart(e) { + if (currentStep !== 2) return; + isStirring = true; + lastAngle = getAngle(e); + } + + function onStirMove(e) { + if (currentStep !== 2 || !isStirring) return; + e.preventDefault(); + const angle = getAngle(e); + if (lastAngle !== null) { + let diff = angle - lastAngle; + // Handle wrap-around + if (diff < -180) diff += 360; + if (diff > 180) diff -= 360; + + // Only count clockwise + if (diff > 0) { + accumulatedAngle += diff; + if (accumulatedAngle >= 360) { + accumulatedAngle -= 360; + incrementStir(); + } + } + } + lastAngle = angle; + } + + function onStirEnd() { + isStirring = false; + lastAngle = null; + } + + function incrementStir() { + completedStirs++; + stirCountSpan.innerText = completedStirs; + if (window.soundFX) window.soundFX.playBottleClink(); + + if (completedStirs >= 3) { + stirCircle.classList.remove('active'); + stirCounterText.style.display = 'none'; + setTimeout(startStep3, 600); + } + } + + // Direct tap fallback for mobile + cauldronZone.addEventListener('click', () => { + if (currentStep === 2 && !isStirring) { + incrementStir(); + } + }); + + cauldronZone.addEventListener('mousedown', onStirStart); + window.addEventListener('mousemove', onStirMove); + window.addEventListener('mouseup', onStirEnd); + + cauldronZone.addEventListener('touchstart', onStirStart, { passive: false }); + window.addEventListener('touchmove', onStirMove, { passive: false }); + window.addEventListener('touchend', onStirEnd); + + // Step 3: Heat Control + let currentTemp = 35; // percent (0-100) + let heatTimer = 0.0; + let heatInterval = null; + + function startStep3() { + currentStep = 3; + stepBadge.innerText = "Schritt 3: Hitze balancieren"; + stepInstruction.innerText = "Pumpe mit dem Blasebalg, um die Hitze im grünen Bereich zu halten!"; + stepSubtext.innerText = "Halte die Nadel für 4 Sekunden im grünen Bereich (60% - 85%)."; + potionSurface.className = 'potion-surface state-heat'; + heatPanel.classList.add('active'); + + if (window.soundFX) window.soundFX.playWhoosh(); + + heatInterval = setInterval(updateHeatLoop, 50); + } + + window.pumpHeat = function() { + if (currentStep !== 3) return; + currentTemp = Math.min(100, currentTemp + 15); + if (window.soundFX) window.soundFX.playWhoosh(); + }; + + function updateHeatLoop() { + if (currentStep !== 3) return; + + // Slow cooling down + currentTemp = Math.max(0, currentTemp - 0.55); + tempIndicator.style.width = currentTemp + '%'; + + // Check ideal zone (60% to 85%) + if (currentTemp >= 60 && currentTemp <= 85) { + heatTimer += 0.05; + heatTimerSpan.innerText = heatTimer.toFixed(1); + tempIndicator.style.background = '#2ecc71'; + } else { + tempIndicator.style.background = 'linear-gradient(90deg, #3498db, #f39c12, #e74c3c)'; + } + + if (heatTimer >= 4.0) { + clearInterval(heatInterval); + finishPotionBrew(); + } + } + + // Step 4: Victory + async function finishPotionBrew() { + currentStep = 4; + stepBadge.innerText = "✅ Trank vollendet!"; + stepBadge.style.background = "rgba(46, 204, 113, 0.3)"; + stepBadge.style.borderColor = "#2ecc71"; + stepBadge.style.color = "#2ecc71"; + + stepInstruction.innerText = "Felix Felicis strahlt in reinem Gold!"; + stepSubtext.innerText = "Dein Zaubertrank ist perfekt gelungen."; + + heatPanel.classList.remove('active'); + potionSurface.className = 'potion-surface state-gold'; + + if (window.soundFX) window.soundFX.playMagicSuccess(); + + // API Completion Call + 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); + } + } catch (e) { + console.error("Brew complete error:", e); + } + + successPanel.style.display = 'block'; + } +})(); diff --git a/app/static/js/cryptex.js b/app/static/js/cryptex.js new file mode 100755 index 0000000..ba8cdd2 --- /dev/null +++ b/app/static/js/cryptex.js @@ -0,0 +1,137 @@ +// Magisches Runen-Drehschloss (Cryptex Engine) +(function() { + const DIALS = [ + // Dial 0: Houses + [ + { icon: '🦁', name: 'Löwe' }, + { icon: '🐍', name: 'Schlange' }, + { icon: '🦅', name: 'Rabe' }, + { icon: '🦡', name: 'Dachs' } + ], + // Dial 1: Astral + [ + { icon: '☀️', name: 'Sonne' }, + { icon: '🌙', name: 'Mond' }, + { icon: '⭐', name: 'Stern' }, + { icon: '⚡', name: 'Blitz' } + ], + // Dial 2: Elements + [ + { icon: '🔥', name: 'Feuer' }, + { icon: '💧', name: 'Wasser' }, + { icon: '🌿', name: 'Kräuter' }, + { icon: '💨', name: 'Wind' } + ], + // Dial 3: Artifacts + [ + { icon: '🗝️', name: 'Schlüssel' }, + { icon: '🪄', name: 'Zauberstab' }, + { icon: '📜', name: 'Rolle' }, + { icon: '⚗️', name: 'Phiole' } + ] + ]; + + // Initial indices (unsolved) + const state = [0, 0, 1, 0]; + let isUnlocked = false; + + // Elements + const feedbackBox = document.getElementById('cryptexFeedback'); + const successPanel = document.getElementById('cryptexSuccess'); + const unlockBtn = document.getElementById('unlockCryptexBtn'); + + window.stepDial = function(dialIdx, dir) { + if (isUnlocked) return; + + const dial = DIALS[dialIdx]; + state[dialIdx] = (state[dialIdx] + dir + dial.length) % dial.length; + + const symEl = document.getElementById(`sym${dialIdx}`); + const lblEl = document.getElementById(`lbl${dialIdx}`); + + // Quick flip animation + symEl.style.transform = dir > 0 ? 'translateY(15px) scale(0.8)' : 'translateY(-15px) scale(0.8)'; + symEl.style.opacity = '0.3'; + + setTimeout(() => { + const currentItem = dial[state[dialIdx]]; + symEl.innerText = currentItem.icon; + lblEl.innerText = currentItem.name; + symEl.style.transform = 'translateY(0) scale(1)'; + symEl.style.opacity = '1'; + }, 100); + + if (window.soundFX) window.soundFX.playStoneSlide(); + }; + + window.checkCryptex = async function() { + if (isUnlocked) return; + + const currentCombination = [ + DIALS[0][state[0]].icon, + DIALS[1][state[1]].icon, + DIALS[2][state[2]].icon, + DIALS[3][state[3]].icon + ]; + + try { + const res = await fetch('/api/cryptex/unlock', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ combination: currentCombination }) + }); + const data = await res.json(); + + if (data.success) { + isUnlocked = true; + unlockBtn.style.display = 'none'; + + if (window.soundFX) { + window.soundFX.playKeyChime(); + window.soundFX.playMagicSuccess(); + } + + // Show golden shine on cylinders + document.querySelectorAll('.dial-cylinder').forEach(c => { + c.style.borderColor = 'var(--gold)'; + c.style.boxShadow = '0 0 20px var(--gold)'; + }); + + if (feedbackBox) { + feedbackBox.style.display = 'block'; + feedbackBox.style.background = 'rgba(46, 204, 113, 0.25)'; + feedbackBox.style.border = '1px solid #2ecc71'; + feedbackBox.style.color = '#2ecc71'; + feedbackBox.innerText = data.message; + } + + if (data.new_item && window.notifyNewItem) { + window.notifyNewItem(data.new_item); + } + + setTimeout(() => { + if (successPanel) successPanel.style.display = 'block'; + }, 800); + + } else { + if (window.soundFX) window.soundFX.playWhoosh(); + + const box = document.querySelector('.cryptex-box'); + box.style.transform = 'translateX(-8px)'; + setTimeout(() => box.style.transform = 'translateX(8px)', 80); + setTimeout(() => box.style.transform = 'translateX(-4px)', 160); + setTimeout(() => box.style.transform = 'translateX(0)', 240); + + if (feedbackBox) { + feedbackBox.style.display = 'block'; + feedbackBox.style.background = 'rgba(231, 76, 60, 0.25)'; + feedbackBox.style.border = '1px solid #e74c3c'; + feedbackBox.style.color = '#e74c3c'; + feedbackBox.innerText = data.message; + } + } + } catch (e) { + console.error("Cryptex check error:", e); + } + }; +})(); diff --git a/app/static/js/inventory.js b/app/static/js/inventory.js index 11ab456..81e2111 100755 --- a/app/static/js/inventory.js +++ b/app/static/js/inventory.js @@ -156,6 +156,18 @@ } else if (action === 'broom_polish') { if (window.soundFX) window.soundFX.playWhoosh(); showFloatingToast("🧹 Der Nimbus 2000 glänzt wie neu. Die Zweige sind perfekt gestutzt!"); + } else if (action === 'spark_wand') { + if (window.soundFX) window.soundFX.playWhoosh(); + showFloatingToast("⚡ Magische Funken sprühen aus deiner Zauberstabspitze!"); + } else if (action === 'lucky_aura') { + if (window.soundFX) window.soundFX.playMagicSuccess(); + showFloatingToast("🍀 Ein Tropfen Felix Felicis! Eine Welle goldenen Glücks erfüllt deine Adern!"); + } else if (action === 'inspect_key') { + if (window.soundFX) window.soundFX.playKeyChime(); + showFloatingToast("🗝️ Die Gravur der vier Gründer leuchtet warm auf: 'Hogwarts, Hogwarts, warzenschweinig alt...'"); + } else if (action === 'poke_jar') { + if (window.soundFX) window.soundFX.playWhoosh(); + showFloatingToast("🧚 *Klopf-Klopf!* Der blaue Wichtel im Glas verzieht frech das Gesicht!"); } else if (action === 'examine_key') { if (window.soundFX) window.soundFX.playKeyChime(); showFloatingToast("🗝️ Der alte Silberschlüssel ist schwer und trägt Spuren vergangener Kämpfe."); diff --git a/app/static/js/pixies.js b/app/static/js/pixies.js new file mode 100755 index 0000000..2ff786f --- /dev/null +++ b/app/static/js/pixies.js @@ -0,0 +1,188 @@ +// Wichtel-Panik / Cornish Pixies Arcade Engine +(function() { + let score = 0; + const targetScore = 10; + let timeLeft = 35; + let gameRunning = false; + let timerInterval = null; + let spawnTimeout = null; + + // DOM Elements + const scoreEl = document.getElementById('pixieScore'); + const timerEl = document.getElementById('pixieTimer'); + const startBtn = document.getElementById('startPixieBtn'); + const startOverlay = document.getElementById('startOverlay'); + const successPanel = document.getElementById('pixieSuccess'); + const holes = document.querySelectorAll('.pixie-hole'); + + const activeTargets = new Map(); // holeIndex -> { type: 'pixie'|'neville', timer: id } + + window.startPixieHunt = function() { + if (gameRunning) return; + gameRunning = true; + score = 0; + timeLeft = 35; + + scoreEl.innerText = score; + timerEl.innerText = timeLeft; + startOverlay.style.display = 'none'; + if (successPanel) successPanel.style.display = 'none'; + + if (window.soundFX) window.soundFX.playWhoosh(); + + // Start countdown + timerInterval = setInterval(() => { + timeLeft--; + timerEl.innerText = timeLeft; + + if (timeLeft <= 0) { + endGame(false); + } + }, 1000); + + scheduleNextSpawn(); + }; + + function scheduleNextSpawn() { + if (!gameRunning) return; + const delay = Math.random() * 400 + 400; // 400ms - 800ms + spawnTimeout = setTimeout(() => { + spawnTarget(); + scheduleNextSpawn(); + }, delay); + } + + function spawnTarget() { + if (!gameRunning) return; + + // Find available holes + const available = []; + for (let i = 0; i < 9; i++) { + if (!activeTargets.has(i)) available.push(i); + } + if (available.length === 0) return; + + const holeIdx = available[Math.floor(Math.random() * available.length)]; + const targetEl = document.getElementById(`target${holeIdx}`); + if (!targetEl) return; + + // 15% chance Neville, 85% chance Pixie + const isNeville = Math.random() < 0.15; + targetEl.className = 'pixie-target active'; + targetEl.innerText = isNeville ? '👦' : '🧚'; + if (isNeville) targetEl.classList.add('neville'); + + const stayTime = Math.random() * 400 + 850; // 850ms - 1250ms + + const hideTimer = setTimeout(() => { + hideTarget(holeIdx); + }, stayTime); + + activeTargets.set(holeIdx, { + isNeville: isNeville, + timer: hideTimer, + clicked: false + }); + } + + function hideTarget(holeIdx) { + const targetEl = document.getElementById(`target${holeIdx}`); + if (targetEl) { + targetEl.classList.remove('active'); + targetEl.classList.remove('frozen'); + targetEl.classList.remove('neville'); + } + activeTargets.delete(holeIdx); + } + + // Touch / Click Handler on holes + holes.forEach(hole => { + const idx = parseInt(hole.dataset.idx, 10); + + function handleTap(e) { + if (!gameRunning) return; + e.preventDefault(); + + if (!activeTargets.has(idx)) return; + const targetData = activeTargets.get(idx); + if (targetData.clicked) return; + targetData.clicked = true; + + const targetEl = document.getElementById(`target${idx}`); + + if (targetData.isNeville) { + // Clicked Neville -> Penalty! + if (window.soundFX) window.soundFX.playWhoosh(); + score = Math.max(0, score - 2); + scoreEl.innerText = score; + + targetEl.style.transform = 'scale(1.3) rotate(15deg)'; + if (window.showFloatingToast) { + window.showFloatingToast("🤕 Neville: »Aua! Ich bin doch kein Wichtel!« (-2 Pkt)"); + } + + setTimeout(() => hideTarget(idx), 350); + + } else { + // Clicked Pixie -> Immobilus! + clearTimeout(targetData.timer); + targetEl.classList.add('frozen'); + targetEl.innerText = '❄️'; + + if (window.soundFX) { + window.soundFX.playKeyChime(); + } + + score++; + scoreEl.innerText = score; + + if (score >= targetScore) { + endGame(true); + } else { + setTimeout(() => hideTarget(idx), 400); + } + } + } + + hole.addEventListener('click', handleTap); + hole.addEventListener('touchstart', handleTap, { passive: false }); + }); + + async function endGame(won) { + gameRunning = false; + clearInterval(timerInterval); + clearTimeout(spawnTimeout); + + // Hide all active + activeTargets.forEach((val, idx) => hideTarget(idx)); + activeTargets.clear(); + + if (won) { + if (window.soundFX) window.soundFX.playMagicSuccess(); + + try { + const res = await fetch('/api/pixies/complete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ caught: score }) + }); + const data = await res.json(); + if (data.new_item && window.notifyNewItem) { + window.notifyNewItem(data.new_item); + } + } catch (e) { + console.error("Pixies complete error:", e); + } + + if (successPanel) successPanel.style.display = 'block'; + + } else { + if (window.soundFX) window.soundFX.playWhoosh(); + startOverlay.style.display = 'block'; + startBtn.innerText = "Erneut versuchen 🔄"; + if (window.showFloatingToast) { + window.showFloatingToast("⏰ Die Zeit ist abgelaufen! Die Wichtel haben gewonnen. Versuch es noch einmal!"); + } + } + } +})(); diff --git a/app/templates/base.html b/app/templates/base.html index b9de723..4426ee6 100755 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -126,6 +126,18 @@ 🪄 Spruch-Duell: {% if state.duel_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %} +
+ ⚗️ Trank-Brauküche: + {% if state.brew_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %} +
+
+ 🔐 Runen-Drehschloss: + {% if state.cryptex_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %} +
+
+ 🧚 Wichtel-Panik: + {% if state.pixies_solved %}✅ Gemeistert{% else %}⏳ Offen{% endif %} +
🧪 Snapes Flammen: {% if state.bottles_solved %}✅ Gelöst{% else %}⏳ Offen{% endif %} @@ -161,15 +173,34 @@ {% if state.duel_solved %}✅ Gemeistert{% else %}⚡ Zaubergesten zeichnen{% endif %}
+ + ⚗️ +
+ Kammer III: Die Brauküche + {% if state.brew_solved %}✅ Gemeistert{% else %}🧪 Felix Felicis brauen{% endif %} +
+
+ + 🔐 +
+ Kammer IV: Das Drehschloss + {% if state.cryptex_solved %}✅ Gemeistert{% else %}🔑 Gründer-Siegel knacken{% endif %} +
+
+ + 🧚 +
+ Kammer V: Wichtel-Panik + {% if state.pixies_solved %}✅ Gemeistert{% else %}❄️ Immobilus auf Wichtel{% endif %} +
+
🧪
- Kammer III: Snapes Flaschen + Kammer VI: Snapes Flaschen {% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Die Flammenkammer{% endif %}
- - 🧩
diff --git a/app/templates/brew.html b/app/templates/brew.html new file mode 100755 index 0000000..73b6ddc --- /dev/null +++ b/app/templates/brew.html @@ -0,0 +1,105 @@ +{% extends "base.html" %} + +{% block title %}Kammer III: Die Zaubertrank-Brauküche · Hogwarts Escape{% endblock %} + +{% block content %} +
+

⚗️ Kammer III: Die Zaubertrank-Brauküche ⚗️

+

+ "Dichte Dämpfe steigen aus dem massiven Kupferkessel auf.
+ Um das nächste Kerkertor aufzuschmelzen, musst du einen legendären Trank vollenden:
+ Felix Felicis – Das flüssige Gold des Glücks!" +

+
+ +
+ +
+ Schritt 1: Zutaten zugeben +

+ Gib die 4 Essenzen in den Kessel! +

+

+ Tippe auf die Zutaten im Regal, um sie in den brodelnden Sud zu geben. +

+
+ + +
+ +
+ ↻ +
+
+ +
+
+
+
🔥
+
+ + + + + +
+ + + +
+ + +
+
+ 🌡️ Halte die Kesseltemperatur für 4 Sekunden im grünen Bereich: +
+
+
+
+
+
+ Kalt (0°C) + Ideal (70-90°C) + Überhitzt (120°C) +
+
+ Ideale Hitze gehalten: 0.0 / 4.0s +
+ +
+ + +
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/cryptex.html b/app/templates/cryptex.html new file mode 100755 index 0000000..89ef2cd --- /dev/null +++ b/app/templates/cryptex.html @@ -0,0 +1,99 @@ +{% extends "base.html" %} + +{% block title %}Kammer IV: Das Runen-Drehschloss der Gründer · Hogwarts Escape{% endblock %} + +{% block content %} +
+

🔐 Kammer IV: Das Runen-Drehschloss 🔐

+

+ "Vor dir ragt ein gewaltiges Tresortor mit einem massiven Messing-Zylinderschloss.
+ In die Steinwand über dem Schloss ist ein altes Gründer-Rätsel gemeißelt:

+ »Vier Siegel schloss der Gründer Rat,
+ bevor man je die Kammer betrat.
+ Der Löwe wacht am Himmelstor,
+ wo nachts die Silbersichel glimmt,
+ das ew’ge Feuer niemals stirbt,
+ bis der Zauberstab die Siegel bricht.«
" +

+
+ +
+ + + + +
+
+
+ +
+ +
+
🦁
+
Löwe
+
+ +
+ + +
+ +
+
☀️
+
Sonne
+
+ +
+ + +
+ +
+
💧
+
Wasser
+
+ +
+ + +
+ +
+
🗝️
+
Schlüssel
+
+ +
+
+
+ + +
+ +
+ + + +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 828405a..95b44d8 100755 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -49,14 +49,55 @@ Duell beginnen 🪄
+ +
+ + {% if state.brew_solved %}✅ Gemeistert{% else %}⚗️ Bereit{% endif %} + +
⚗️🧪
+

Kammer III: Die Brauküche

+

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

+ Kessel anheizen ⚗️ +
- + +
+ + {% if state.cryptex_solved %}✅ Gemeistert{% else %}🔐 Bereit{% endif %} + +
🔐🗝️
+

Kammer IV: Das Drehschloss

+

+ Das massive Tresortor der vier Hogwarts-Gründer. Drehe die vier Messingzylinder mit Wappen, Mond und Sternen, + um das alchemistische Rätsel zu lösen und den Gründerschlüssel zu befreien! +

+ Ringe drehen 🔐 +
+ + +
+ + {% if state.pixies_solved %}✅ Gemeistert{% else %}🧚 Bereit{% endif %} + +
🧚❄️
+

Kammer V: Wichtel-Panik

+

+ Chaos im Klassenzimmer! Freche blaue Wichtel wirbeln umher. Schnelle Reflexe sind gefragt: + Banne 10 Wichtel mit dem Einfrier-Zauber »Immobilus!« und schone Neville! +

+ Wichtel jagen 🧚 +
+ +
{% if state.bottles_solved %}✅ Gemeistert{% else %}🔥 Bereit{% endif %}
🧪🔥
-

Kammer III: Snapes Flaschen

+

Kammer VI: 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. diff --git a/app/templates/pixies.html b/app/templates/pixies.html new file mode 100755 index 0000000..357c41b --- /dev/null +++ b/app/templates/pixies.html @@ -0,0 +1,103 @@ +{% extends "base.html" %} + +{% block title %}Kammer V: Wichtel-Panik · Hogwarts Escape{% endblock %} + +{% block content %} +

+

🧚 Kammer V: Wichtel-Panik 🧚

+

+ "Lockharts Eisenkäfig ist aufgeplatzt! Ein Schwarm frecher kornischer Wichtel wirbelt durch den Raum.
+ Banne 10 Wichtel mit dem Einfrier-Zauber »Immobilus!« ❄️ mit schnellen Fingern!
+ Vorsicht: Tippe nicht auf Neville Longbottom (👦), der an der Decke baumelt!" +

+
+ +
+ +
+
+ 🧚 Gefangen: 0 / 10 +
+
+ ⏳ Zeit: 35s +
+
+ + +
+ +
+ 📚 +
🧚
+
+ +
+ 🗝️ +
🧚
+
+ +
+ 🕯️ +
🧚
+
+ +
+ 🧪 +
🧚
+
+ +
+ 📦 +
🧚
+
+ +
+ 🪟 +
🧚
+
+ +
+ 📜 +
🧚
+
+ +
+ 🕸️ +
🧚
+
+ +
+ 🕰️ +
🧚
+
+
+ + +
+ +
+ + + +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/test_app.py b/test_app.py index f8199f8..2c436fb 100755 --- a/test_app.py +++ b/test_app.py @@ -24,6 +24,20 @@ def test_endpoints(): assert res.status_code in [301, 302] assert res.headers["location"] == "/duel" + # Test new mini-games: Brew, Cryptex, Pixies + res = client.get("/brew") + assert res.status_code == 200 + assert "Die Zaubertrank-Brauküche" in res.text + assert "Felix Felicis" in res.text + + res = client.get("/cryptex") + assert res.status_code == 200 + assert "Das Runen-Drehschloss" in res.text + + res = client.get("/pixies") + assert res.status_code == 200 + assert "Wichtel-Panik" in res.text + res = client.get("/bottles") assert res.status_code == 200 assert "Snapes Flaschenrätsel" in res.text @@ -55,8 +69,7 @@ def test_endpoints(): # 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 res.json()["success"] is True # Test API Duel complete (adds duel_badge) res = client.post("/api/duel/complete") @@ -64,14 +77,53 @@ def test_endpoints(): 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 Brew complete (adds felix_felicis) + res = client.post("/api/brew/complete") + assert res.status_code == 200 + brew_data = res.json() + assert brew_data["success"] is True + assert brew_data["new_item"]["id"] == "felix_felicis" + + # Test API Cryptex unlock (wrong then correct) + res = client.post("/api/cryptex/unlock", json={"combination": ["🐍", "☀️", "💧", "🗝️"]}) + assert res.status_code == 200 + assert res.json()["success"] is False + + res = client.post("/api/cryptex/unlock", json={"combination": ["🦁", "🌙", "🔥", "🪄"]}) + assert res.status_code == 200 + cryp_data = res.json() + assert cryp_data["success"] is True + assert cryp_data["new_item"]["id"] == "cryptex_key" + + # Test API Pixies complete (under 10 then >= 10) + res = client.post("/api/pixies/complete", json={"caught": 5}) + assert res.status_code == 200 + assert res.json()["success"] is False + + res = client.post("/api/pixies/complete", json={"caught": 10}) + assert res.status_code == 200 + pix_data = res.json() + assert pix_data["success"] is True + assert pix_data["new_item"]["id"] == "pixie_jar" + + # Verify updated inventory contains all items + res = client.get("/api/inventory") + updated_inv = res.json() + inv_ids = [item["id"] for item in updated_inv["inventory"]] + assert "duel_badge" in inv_ids + assert "felix_felicis" in inv_ids + assert "cryptex_key" in inv_ids + assert "pixie_jar" in inv_ids + assert updated_inv["brew_solved"] is True + assert updated_inv["cryptex_solved"] is True + assert updated_inv["pixies_solved"] is True # 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 drink_data = res.json() assert drink_data["result"] == "success_ahead" - assert "new_item" in drink_data assert drink_data["new_item"]["id"] == "philosophers_stone" # Test API Reset @@ -79,7 +131,7 @@ def test_endpoints(): assert res.status_code == 200 assert res.json()["success"] is True - print("ALL TESTS (RING FLIGHT & SPELL DUEL) PASSED! ⭕🪄✨") + print("ALL TESTS (ALL MINI-GAMES & APIS) PASSED! ⭕🪄⚗️🔐🧚🧪💎✨") if __name__ == "__main__": test_endpoints()